diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index 936e656c0c45a3ca1badd9de8c41d9af534f2282..c706c6d1263ae6e9850b1a1cf65965e8134758f7 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -375,6 +375,18 @@ msgstr "Zeitzone in der die Konfernez veranstaltet wird" msgid "Conference__timezone" msgstr "Zeitzone" +msgid "Conference__send_pn_disabled__help" +msgstr "PN-Versand in dieser Konferenz deaktivieren" + +msgid "Conference__send_pn_disabled" +msgstr "PNs deaktiviert" + +msgid "Conference__board_disabled__help" +msgstr "Einträge im Board erstellen / bearbeiten deaktiviert" + +msgid "Conference__board_disabled" +msgstr "Bulletin Board deaktiviert" + msgid "Conference__support_clusters__help" msgstr "Assemblies können sich zu Clustern gruppieren" @@ -1095,6 +1107,12 @@ msgstr "im WorkAdventure wird Video von anderen Teilnehmern gezeigt" msgid "PlatformUser__receive_video" msgstr "Video-Chat" +msgid "PlatformUser__shadow_banned__help" +msgstr "User kann PNs verschicken aber die Empfänger werden diese nie zu sehen bekommen" + +msgid "PlatformUser__shadow_banned" +msgstr "Shadowbanned" + msgid "PlatformUser__autoaccept_contacts__help" msgstr "Kontaktanfragen automatisch akzeptieren" diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index 7037487469959983ca8f0ba7da1935ff3c512554..ab9303974afc4609226cd3b6f21d3e05569828c9 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -381,6 +381,18 @@ msgstr "time zone in which this conference is operated" msgid "Conference__timezone" msgstr "time zone" +msgid "Conference__send_pn_disabled__help" +msgstr "disable sending of private messages in this conference" + +msgid "Conference__send_pn_disabled" +msgstr "disable PMs" + +msgid "Conference__board_disabled__help" +msgstr "creating / editing board entries disabled" + +msgid "Conference__board_disabled" +msgstr "disable bulletin board" + msgid "Conference__support_clusters__help" msgstr "allow assemblies to cluster in groups" @@ -1101,6 +1113,12 @@ msgstr "see video from other participants in e.g. WorkAdventure" msgid "PlatformUser__receive_video" msgstr "video chat" +msgid "PlatformUser__shadow_banned__help" +msgstr "User will be able to send DMs but recipients won't see them" + +msgid "PlatformUser__shadow_banned" +msgstr "shadow banned" + msgid "PlatformUser__autoaccept_contacts__help" msgstr "automatically accept incoming contact requests" diff --git a/src/core/migrations/0055_dm_restrictions.py b/src/core/migrations/0055_dm_restrictions.py new file mode 100644 index 0000000000000000000000000000000000000000..e898a9e2bafa5156d65afb7785ed830a9b1afeb3 --- /dev/null +++ b/src/core/migrations/0055_dm_restrictions.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.4 on 2020-12-29 00:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0054_UserBadgeGrammar'), + ] + + operations = [ + migrations.AddField( + model_name='conference', + name='board_disabled', + field=models.BooleanField(default=False, help_text='Conference__board_disabled__help', verbose_name='Conference__board_disabled'), + ), + migrations.AddField( + model_name='conference', + name='send_pn_disabled', + field=models.BooleanField(default=False, help_text='Conference__send_pn_disabled__help', verbose_name='Conference__send_pn_disabled'), + ), + migrations.AddField( + model_name='platformuser', + name='shadow_banned', + field=models.BooleanField(default=False, help_text='PlatformUser__shadow_banned__help', verbose_name='PlatformUser__shadow_banned'), + ), + ] diff --git a/src/core/models/conference.py b/src/core/models/conference.py index 922246c2f1b6a2113fab09dc3c9041e8a4b91c19..e15c0900e11035ed1bb1a5fa2108a05a29da07ef 100644 --- a/src/core/models/conference.py +++ b/src/core/models/conference.py @@ -160,6 +160,15 @@ class Conference(models.Model): help_text=_('Conference__timezone__help'), verbose_name=_('Conference__timezone')) + send_pn_disabled = models.BooleanField( + default=False, + help_text=_('Conference__send_pn_disabled__help'), + verbose_name=_('Conference__send_pn_disabled')) + board_disabled = models.BooleanField( + default=False, + help_text=_('Conference__board_disabled__help'), + verbose_name=_('Conference__board_disabled')) + support_clusters = models.BooleanField( default=True, help_text=_('Conference__support_clusters__help'), diff --git a/src/core/models/users.py b/src/core/models/users.py index 88b362e36b073bd636bde48e19e5da5af06aaa25..e11667e6f8d99217ad924bbedead48b49b073b1c 100644 --- a/src/core/models/users.py +++ b/src/core/models/users.py @@ -114,6 +114,12 @@ class PlatformUser(AbstractUser): help_text=_('PlatformUser__receive_video__help'), verbose_name=_('PlatformUser__receive_video')) + # Administrative + shadow_banned = models.BooleanField( + default=False, + help_text=_('PlatformUser__shadow_banned__help'), + verbose_name=_('PlatformUser__shadow_banned')) + audio_muted = models.BooleanField(default=False) audio_volume = models.FloatField(blank=True, null=True) diff --git a/src/plainui/forms.py b/src/plainui/forms.py index 3a91ebcc548fa04e793ef36b3496677bd0e077c3..9be1421737831356d2589ce9b00962d363af8d50 100644 --- a/src/plainui/forms.py +++ b/src/plainui/forms.py @@ -25,17 +25,35 @@ class UsernameField(forms.CharField): class NewDirectMessageForm(forms.Form): + def __init__(self, conf, user, *args, **kwargs): + self.conf = conf + self.user = user + super().__init__(*args, **kwargs) + in_reply_to = forms.UUIDField(required=False) recipient = UsernameField(widget=forms.TextInput(attrs={'placeholder': _("Please enter the recipient name")})) subject = forms.CharField(max_length=200, min_length=1, strip=True, widget=forms.TextInput(attrs={'placeholder': _("Please enter a subject")})) body = forms.CharField(widget=forms.Textarea) + def clean(self): + if self.conf.send_pn_disabled: + raise ValidationError(_("Sending Messages is currently disabled")) + class BulletinBoardEntryForm(forms.Form): + def __init__(self, conf, user, *args, **kwargs): + self.conf = conf + self.user = user + super().__init__(*args, **kwargs) + title = forms.CharField(max_length=200, min_length=1, strip=True, widget=forms.TextInput(attrs={'placeholder': _("Please enter a title")})) is_public = forms.BooleanField(required=False) text = forms.CharField(widget=forms.Textarea) + def clean(self): + if self.conf.board_disabled: + raise ValidationError(_("Bulletin Board is currently disabled")) + class ExampleForm(forms.Form): """ used in the component gallery """ @@ -247,7 +265,7 @@ class ReportForm(forms.Form): if kind == 'url': reported_content = self.cleaned_data['kind_data'] elif kind == 'pn': - dm = DirectMessage.objects.get(pk=self.cleaned_data['kind_data'], recipient=request.user) + dm = DirectMessage.objects.get(pk=self.cleaned_data['kind_data']) reported_content = 'DM by %(sender)s (%(sender_id)s) to %(recipient)s (%(recipient_id)s) at %(timestamp)s (%(uuid)s).\n' reported_content += 'Subject: "%(subject)s"\nMessage: "%(body)s"' reported_content %= { diff --git a/src/plainui/locale/de/LC_MESSAGES/django.po b/src/plainui/locale/de/LC_MESSAGES/django.po index fde3b102317a7b2c4f28d8e970c3cf469a1e2df9..61976732b7d7f66f30777d1b95707c5b035cd3f7 100644 --- a/src/plainui/locale/de/LC_MESSAGES/django.po +++ b/src/plainui/locale/de/LC_MESSAGES/django.po @@ -27,9 +27,15 @@ msgstr "Bitte einen Empfänger angeben" msgid "Please enter a subject" msgstr "Bitte einen Betreff eingeben" +msgid "Sending Messages is currently disabled" +msgstr "Nachrichten versenden ist momentan deaktiviert" + msgid "Please enter a title" msgstr "Bitte einen Titel eingeben" +msgid "Bulletin Board is currently disabled" +msgstr "Das Board ist momentan deaktiviert" + msgid "Placeholder text" msgstr "Platzhalter text" diff --git a/src/plainui/locale/en/LC_MESSAGES/django.po b/src/plainui/locale/en/LC_MESSAGES/django.po index e4cdd8becb1211e87fc0f75599a14448442a896c..062a82002da542844fe87358968ef7a176b6f162 100644 --- a/src/plainui/locale/en/LC_MESSAGES/django.po +++ b/src/plainui/locale/en/LC_MESSAGES/django.po @@ -27,9 +27,15 @@ msgstr "" msgid "Please enter a subject" msgstr "" +msgid "Sending Messages is currently disabled" +msgstr "" + msgid "Please enter a title" msgstr "" +msgid "Bulletin Board is currently disabled" +msgstr "" + msgid "Placeholder text" msgstr "" diff --git a/src/plainui/tests.py b/src/plainui/tests.py index c344d4f6ab58d85554854d2c7a68ff7e22e61bd7..ad4649148ec3e55ae11ebdbed8c4c8d56d25fc0c 100644 --- a/src/plainui/tests.py +++ b/src/plainui/tests.py @@ -1033,7 +1033,7 @@ class ViewsTest(TestCase): self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['form']['recipient'].value(), 'blgl') - @override_settings(LANGUAGE_CODE='en') + @override_settings(LANGUAGE_CODE='en', AUTOBAN_KEYWORDS=['blork', re.compile('12.45')]) def test_PersonalMessageSendView_post(self): user2 = PlatformUser(username='testuser2') user2.save() @@ -1085,6 +1085,38 @@ class ViewsTest(TestCase): self.assertEqual(form.errors['subject'], ['This field is required.']) self.assertEqual(form.errors['body'], ['This field is required.']) + DirectMessage.objects.all().delete() + self.user.refresh_from_db() + self.assertFalse(self.user.shadow_banned) + self.assertEqual(len(mail.outbox), 0) + resp = self.client.post(reverse('plainui:personal_message_send', kwargs={'conf_slug': self.conf.slug}), { + 'in_reply_to': '', + 'recipient': 'testuser2', + 'subject': 'blork', + 'body': 'Body2', + }) + self.assertRedirects(resp, reverse('plainui:personal_message', kwargs={'conf_slug': self.conf.slug})) + new_dm = DirectMessage.objects.get() + self.user.refresh_from_db() + self.assertTrue(self.user.shadow_banned) + self.assertTrue(new_dm.deleted_by_recipient, True) + self.assertEqual(len(mail.outbox), 1) + + self.user.shadow_banned = False + self.user.save() + resp = self.client.post(reverse('plainui:personal_message_send', kwargs={'conf_slug': self.conf.slug}), { + 'in_reply_to': '', + 'recipient': 'testuser2', + 'subject': 'this is ok', + 'body': '12345', + }) + self.assertRedirects(resp, reverse('plainui:personal_message', kwargs={'conf_slug': self.conf.slug})) + new_dm = DirectMessage.objects.get(subject='this is ok') + self.user.refresh_from_db() + self.assertTrue(self.user.shadow_banned) + self.assertTrue(new_dm.deleted_by_recipient, True) + self.assertEqual(len(mail.outbox), 2) + def test_PersonalMessageShowView(self): user2 = PlatformUser(username='testuser2') user2.save() @@ -1357,7 +1389,7 @@ class ViewsTest(TestCase): self.assertEqual(resp.context_data['form']['title'].value(), '') self.assertEqual(resp.context_data['form']['text'].value(), '') - @override_settings(LANGUAGE_CODE='en') + @override_settings(LANGUAGE_CODE='en', AUTOBAN_KEYWORDS=['foo 123', re.compile('asdf.foo')]) def test_BoardEntryEditView_post(self): user2 = PlatformUser(username='testuser2') user2.save() @@ -1412,6 +1444,33 @@ class ViewsTest(TestCase): self.assertEqual(new_entry.text, 'some texty text') self.assertSetsMessage(resp, 'Bulletin Board Entry created.') + BulletinBoardEntry.objects.all().delete() + self.user.refresh_from_db() + self.assertFalse(self.user.shadow_banned) + self.assertEqual(len(mail.outbox), 0) + self.assertNeedsLogin(reverse('plainui:board_entry_new', kwargs={'conf_slug': self.conf.slug}), post=True) + resp = self.client.post(reverse('plainui:board_entry_new', kwargs={'conf_slug': self.conf.slug}), { + 'title': 'New Entry', + 'text': 'foo 123', + }) + new_entry = BulletinBoardEntry.objects.get() + self.assertRedirects(resp, reverse('plainui:board_entry_edit', kwargs={'conf_slug': self.conf.slug, 'id': str(new_entry.pk)})) + self.user.refresh_from_db() + self.assertTrue(self.user.shadow_banned) + self.assertEqual(len(mail.outbox), 1) + + self.user.shadow_banned = False + self.user.save() + self.assertNeedsLogin(reverse('plainui:board_entry_edit', kwargs={'conf_slug': self.conf.slug, 'id': str(new_entry.pk)}), post=True) + resp = self.client.post(reverse('plainui:board_entry_edit', kwargs={'conf_slug': self.conf.slug, 'id': str(new_entry.pk)}), { + 'title': 'asdf4foo', + 'text': 'blblblblbl', + }) + self.assertRedirects(resp, reverse('plainui:board_entry_edit', kwargs={'conf_slug': self.conf.slug, 'id': str(new_entry.pk)})) + self.user.refresh_from_db() + self.assertTrue(self.user.shadow_banned) + self.assertEqual(len(mail.outbox), 2) + def test_BoardEntryDeleteView_get(self): self.assertNeedsLogin(reverse('plainui:board_entry_delete', kwargs={'conf_slug': self.conf.slug})) resp = self.client.get(reverse('plainui:board_entry_delete', kwargs={'conf_slug': self.conf.slug})) diff --git a/src/plainui/utils.py b/src/plainui/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c8455d1d8be8ba7cdae54fb1303ad51df3cff64b --- /dev/null +++ b/src/plainui/utils.py @@ -0,0 +1,35 @@ +import re + +from django.conf import settings + +from .forms import ReportForm + + +def check_message_content(conf, request, text, kind, kind_data): + try: + trigger_autoban = False + for pattern in settings.AUTOBAN_KEYWORDS: + if isinstance(pattern, re.Pattern): + trigger_autoban |= pattern.search(text) is not None + elif pattern in text: + trigger_autoban = True + + if not trigger_autoban: + return False + + request.user.shadow_banned = True + request.user.save(update_fields=['shadow_banned']) + + report_form = ReportForm(conf=conf, data={ + 'kind': kind, + 'kind_data': str(kind_data), + 'category': 'abuse', + 'message': 'Autoban triggered', + 'message2': '.', + }) + report_form.is_valid() + + report_form.send_report_mail('plainui/registration/report_mail_body.txt', request) + return True + except Exception: + raise Exception("Boom") # don't allow leaking eg. ValidationErrors that might be handled in a View diff --git a/src/plainui/views.py b/src/plainui/views.py index 5cf780429b44e6e6fffb3c317ad92e3bb574184a..7c03f25664cc5aaf3ab919f41168eadfa96a6606 100644 --- a/src/plainui/views.py +++ b/src/plainui/views.py @@ -37,6 +37,7 @@ from .forms import BulletinBoardEntryForm, ExampleForm, InputTokenForm, NewDirec ProfileEditForm, SelfOrganizedSessionForm, RedeemTokenAddToUserForm, RedeemTokenUserCreateForm, ReportForm, \ RedeemBadgeForm, TokenPasswortResetForm from .models import BulletinBoardEntry +from .utils import check_message_content def _session_refresh_favorite_assemblies(session, user) -> List[str]: @@ -828,6 +829,12 @@ class PersonalMessageSendView(ConferenceRequiredMixin, FormView): initial['in_reply_to'] = self.request.POST.get('in_reply_to', self.request.GET.get('in_reply_to', '')) return initial + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['conf'] = self.conf + kwargs['user'] = self.request.user + return kwargs + def get_success_url(self): return reverse('plainui:personal_message', kwargs={'conf_slug': self.kwargs['conf_slug']}) @@ -841,11 +848,16 @@ class PersonalMessageSendView(ConferenceRequiredMixin, FormView): in_reply_to_id=in_reply_to, subject=form.cleaned_data['subject'], body=form.cleaned_data['body'], + deleted_by_recipient=self.request.user != form.cleaned_data['recipient'] and self.request.user.shadow_banned, ) dm.save() if in_reply_to: DirectMessage.objects.filter(id=in_reply_to, recipient=self.request.user).update(has_responded=True) + if check_message_content(self.conf, self.request, form.cleaned_data['subject'], 'pn', dm.pk) or \ + check_message_content(self.conf, self.request, form.cleaned_data['body'], 'pn', dm.pk): + dm.deleted_by_recipient = True + dm.save(update_fields=['deleted_by_recipient']) messages.success(self.request, gettext("Message sent.")) return super().form_valid(form) @@ -1074,6 +1086,12 @@ class BoardEntryEditView(ConferenceRequiredMixin, FormView): **kwargs ) + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['conf'] = self.conf + kwargs['user'] = self.request.user + return kwargs + def get_initial(self): initial = super().get_initial() if 'id' in self.kwargs: @@ -1094,6 +1112,9 @@ class BoardEntryEditView(ConferenceRequiredMixin, FormView): self.board_entry.text = form.cleaned_data['text'] self.board_entry.save() + check_message_content(self.conf, self.request, form.cleaned_data['title'], 'board', self.board_entry.pk) + check_message_content(self.conf, self.request, form.cleaned_data['text'], 'board', self.board_entry.pk) + if 'id' in self.kwargs: messages.success(self.request, gettext("Bulletin Board Entry updated.")) else: diff --git a/src/rc3platform/settings/base.py b/src/rc3platform/settings/base.py index 85f85aab470813be34c554a51ee056d435da4ac8..4551ff24a9806cfed2e4ad261d8f31546c25782e 100644 --- a/src/rc3platform/settings/base.py +++ b/src/rc3platform/settings/base.py @@ -189,6 +189,9 @@ REST_FRAMEWORK = { # List of disallowed assembly slugs FORBIDDEN_ASSEMBLY_SLUGS = ['visit', 'maps', 'api', 'pusher'] +# list of autoban keywords. (Usage will trigger a report and send an abuse mail). Use strings or re.compile +AUTOBAN_KEYWORDS = [] + # Mail configuration MAIL_REPLY_TO = [] SUPPORT_HTML_MAILS = False