diff options
Diffstat (limited to 'pydis_site')
21 files changed, 393 insertions, 28 deletions
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 059f52eb..0333fefc 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -12,6 +12,7 @@ from .models import ( MessageDeletionContext, Nomination, OffTopicChannelName, + OffensiveMessage, Role, Tag, User @@ -60,6 +61,7 @@ admin.site.register(Infraction) admin.site.register(LogEntry, LogEntryAdmin) admin.site.register(MessageDeletionContext) admin.site.register(Nomination) +admin.site.register(OffensiveMessage) admin.site.register(OffTopicChannelName) admin.site.register(Role) admin.site.register(Tag) diff --git a/pydis_site/apps/api/migrations/0049_deletedmessage_attachments.py b/pydis_site/apps/api/migrations/0049_deletedmessage_attachments.py new file mode 100644 index 00000000..31ac239a --- /dev/null +++ b/pydis_site/apps/api/migrations/0049_deletedmessage_attachments.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.6 on 2019-10-28 17:12 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0049_offensivemessage'), + ] + + operations = [ + migrations.AddField( + model_name='deletedmessage', + name='attachments', + field=django.contrib.postgres.fields.ArrayField(base_field=models.URLField(max_length=512), default=[], blank=True, help_text='Attachments attached to this message.', size=None), + preserve_default=False, + ), + ] diff --git a/pydis_site/apps/api/migrations/0049_offensivemessage.py b/pydis_site/apps/api/migrations/0049_offensivemessage.py new file mode 100644 index 00000000..fe4a1961 --- /dev/null +++ b/pydis_site/apps/api/migrations/0049_offensivemessage.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.6 on 2019-11-07 18:08 + +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.offensive_message +import pydis_site.apps.api.models.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0048_add_infractions_unique_constraints_active'), + ] + + operations = [ + migrations.CreateModel( + name='OffensiveMessage', + fields=[ + ('id', models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')])), + ('channel_id', models.BigIntegerField(help_text='The channel ID that the message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), + ('delete_date', models.DateTimeField(help_text='The date on which the message will be auto-deleted.', validators=[pydis_site.apps.api.models.bot.offensive_message.future_date_validator])), + ], + bases=(pydis_site.apps.api.models.utils.ModelReprMixin, models.Model), + ), + ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index a4656bc3..450d18cd 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -7,6 +7,7 @@ from .bot import ( Message, MessageDeletionContext, Nomination, + OffensiveMessage, OffTopicChannelName, Reminder, Role, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 46219ea2..8ae47746 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -7,6 +7,7 @@ from .message import Message from .message_deletion_context import MessageDeletionContext from .nomination import Nomination from .off_topic_channel_name import OffTopicChannelName +from .offensive_message import OffensiveMessage from .reminder import Reminder from .role import Role from .tag import Tag diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 31316a01..8b18fc9f 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -51,6 +51,13 @@ class Message(ModelReprMixin, models.Model): ), help_text="Embeds attached to this message." ) + attachments = pgfields.ArrayField( + models.URLField( + max_length=512 + ), + blank=True, + help_text="Attachments attached to this message." + ) @property def timestamp(self) -> datetime: diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py new file mode 100644 index 00000000..b466d9c2 --- /dev/null +++ b/pydis_site/apps/api/models/bot/offensive_message.py @@ -0,0 +1,48 @@ +import datetime + +from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +def future_date_validator(date: datetime.date) -> None: + """Raise ValidationError if the date isn't a future date.""" + if date < datetime.datetime.now(datetime.timezone.utc): + raise ValidationError("Date must be a future date") + + +class OffensiveMessage(ModelReprMixin, models.Model): + """A message that triggered a filter and that will be deleted one week after it was sent.""" + + id = models.BigIntegerField( + primary_key=True, + help_text="The message ID as taken from Discord.", + validators=( + MinValueValidator( + limit_value=0, + message="Message IDs cannot be negative." + ), + ) + ) + channel_id = models.BigIntegerField( + help_text=( + "The channel ID that the message was " + "sent in, taken from Discord." + ), + validators=( + MinValueValidator( + limit_value=0, + message="Channel IDs cannot be negative." + ), + ) + ) + delete_date = models.DateTimeField( + help_text="The date on which the message will be auto-deleted.", + validators=(future_date_validator,) + ) + + def __str__(self): + """Return some info on this message, for display purposes only.""" + return f"Message {self.id}, will be deleted at {self.delete_date}" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 4e7cd863..0d1a4684 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -8,6 +8,7 @@ from .models import ( DocumentationLink, Infraction, LogEntry, MessageDeletionContext, Nomination, OffTopicChannelName, + OffensiveMessage, Reminder, Role, Tag, User ) @@ -49,7 +50,8 @@ class DeletedMessageSerializer(ModelSerializer): fields = ( 'id', 'author', 'channel_id', 'content', - 'embeds', 'deletion_context' + 'embeds', 'deletion_context', + 'attachments' ) @@ -247,3 +249,13 @@ class NominationSerializer(ModelSerializer): fields = ( 'id', 'active', 'actor', 'reason', 'user', 'inserted_at', 'end_reason', 'ended_at') + + +class OffensiveMessageSerializer(ModelSerializer): + """A class providing (de-)serialization of `OffensiveMessage` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = OffensiveMessage + fields = ('id', 'channel_id', 'delete_date') diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index d1e9f2f5..b3a8197b 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -25,14 +25,16 @@ class DeletedMessagesWithoutActorTests(APISubdomainTestCase): 'id': 55, 'channel_id': 5555, 'content': "Terror Billy is a meanie", - 'embeds': [] + 'embeds': [], + 'attachments': [] }, { 'author': cls.author.id, 'id': 56, 'channel_id': 5555, 'content': "If you purge this, you're evil", - 'embeds': [] + 'embeds': [], + 'attachments': [] } ] } @@ -64,7 +66,8 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase): 'id': 12903, 'channel_id': 1824, 'content': "I hate trailing commas", - 'embeds': [] + 'embeds': [], + 'attachments': [] }, ] } diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index b4a766d0..a97d3251 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -12,6 +12,7 @@ from ..models import ( ModelReprMixin, Nomination, OffTopicChannelName, + OffensiveMessage, Reminder, Role, Tag, @@ -69,6 +70,11 @@ class StringDunderMethodTests(SimpleTestCase): DocumentationLink( 'test', 'http://example.com', 'http://example.com' ), + OffensiveMessage( + id=602951077675139072, + channel_id=291284109232308226, + delete_date=dt(3000, 1, 1) + ), OffTopicChannelName(name='bob-the-builders-playground'), Role( id=5, name='test role', diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py new file mode 100644 index 00000000..d5896714 --- /dev/null +++ b/pydis_site/apps/api/tests/test_offensive_message.py @@ -0,0 +1,155 @@ +import datetime + +from django_hosts.resolvers import reverse + +from .base import APISubdomainTestCase +from ..models import OffensiveMessage + + +class CreationTests(APISubdomainTestCase): + def test_accept_valid_data(self): + url = reverse('bot:offensivemessage-list', host='api') + delete_at = datetime.datetime.now() + datetime.timedelta(days=1) + data = { + 'id': '602951077675139072', + 'channel_id': '291284109232308226', + 'delete_date': delete_at.isoformat()[:-1] + } + + aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc) + + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 201) + + offensive_message = OffensiveMessage.objects.get(id=response.json()['id']) + self.assertAlmostEqual( + aware_delete_at, + offensive_message.delete_date, + delta=datetime.timedelta(seconds=1) + ) + self.assertEqual(data['id'], str(offensive_message.id)) + self.assertEqual(data['channel_id'], str(offensive_message.channel_id)) + + def test_returns_400_on_non_future_date(self): + url = reverse('bot:offensivemessage-list', host='api') + delete_at = datetime.datetime.now() - datetime.timedelta(days=1) + data = { + 'id': '602951077675139072', + 'channel_id': '291284109232308226', + 'delete_date': delete_at.isoformat()[:-1] + } + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), { + 'delete_date': ['Date must be a future date'] + }) + + def test_returns_400_on_negative_id_or_channel_id(self): + url = reverse('bot:offensivemessage-list', host='api') + delete_at = datetime.datetime.now() + datetime.timedelta(days=1) + data = { + 'id': '602951077675139072', + 'channel_id': '291284109232308226', + 'delete_date': delete_at.isoformat()[:-1] + } + cases = ( + ('id', '-602951077675139072'), + ('channel_id', '-291284109232308226') + ) + + for field, invalid_value in cases: + with self.subTest(fied=field, invalid_value=invalid_value): + test_data = data.copy() + test_data.update({field: invalid_value}) + + response = self.client.post(url, test_data) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), { + field: ['Ensure this value is greater than or equal to 0.'] + }) + + +class ListTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): # noqa + delete_at = datetime.datetime.now() + datetime.timedelta(days=1) + aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc) + + cls.messages = [ + { + 'id': 602951077675139072, + 'channel_id': 91284109232308226, + }, + { + 'id': 645298201494159401, + 'channel_id': 592000283102674944 + } + ] + + cls.of1 = OffensiveMessage.objects.create( + **cls.messages[0], + delete_date=aware_delete_at.isoformat() + ) + cls.of2 = OffensiveMessage.objects.create( + **cls.messages[1], + delete_date=aware_delete_at.isoformat() + ) + + # Expected API answer : + cls.messages[0]['delete_date'] = delete_at.isoformat() + 'Z' + cls.messages[1]['delete_date'] = delete_at.isoformat() + 'Z' + + def test_get_data(self): + url = reverse('bot:offensivemessage-list', host='api') + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + self.assertEqual(response.json(), self.messages) + + +class DeletionTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): # noqa + delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) + + cls.valid_offensive_message = OffensiveMessage.objects.create( + id=602951077675139072, + channel_id=291284109232308226, + delete_date=delete_at.isoformat() + ) + + def test_delete_data(self): + url = reverse( + 'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) + ) + + response = self.client.delete(url) + self.assertEqual(response.status_code, 204) + + self.assertFalse( + OffensiveMessage.objects.filter(id=self.valid_offensive_message.id).exists() + ) + + +class NotAllowedMethodsTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): # noqa + delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) + + cls.valid_offensive_message = OffensiveMessage.objects.create( + id=602951077675139072, + channel_id=291284109232308226, + delete_date=delete_at.isoformat() + ) + + def test_returns_405_for_patch_and_put_requests(self): + url = reverse( + 'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) + ) + not_allowed_methods = (self.client.patch, self.client.put) + + for method in not_allowed_methods: + with self.subTest(method=method): + response = method(url, {}) + self.assertEqual(response.status_code, 405) diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py index 4222f0c0..241af08c 100644 --- a/pydis_site/apps/api/tests/test_validators.py +++ b/pydis_site/apps/api/tests/test_validators.py @@ -1,7 +1,10 @@ +from datetime import datetime, timezone + from django.core.exceptions import ValidationError from django.test import TestCase from ..models.bot.bot_setting import validate_bot_setting_name +from ..models.bot.offensive_message import future_date_validator from ..models.bot.tag import validate_tag_embed @@ -245,3 +248,12 @@ class TagEmbedValidatorTests(TestCase): 'name': "Bob" } }) + + +class OffensiveMessageValidatorsTests(TestCase): + def test_accepts_future_date(self): + future_date_validator(datetime(3000, 1, 1, tzinfo=timezone.utc)) + + def test_rejects_non_future_date(self): + with self.assertRaises(ValidationError): + future_date_validator(datetime(1000, 1, 1, tzinfo=timezone.utc)) diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index ac6704c8..4a0281b4 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -6,7 +6,8 @@ from .viewsets import ( BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, InfractionViewSet, LogEntryViewSet, NominationViewSet, - OffTopicChannelNameViewSet, ReminderViewSet, + OffTopicChannelNameViewSet, + OffensiveMessageViewSet, ReminderViewSet, RoleViewSet, TagViewSet, UserViewSet ) @@ -34,6 +35,10 @@ bot_router.register( NominationViewSet ) bot_router.register( + 'offensive-messages', + OffensiveMessageViewSet +) +bot_router.register( 'off-topic-channel-names', OffTopicChannelNameViewSet, base_name='offtopicchannelname' diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index f9a186d9..3cf9f641 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -5,6 +5,7 @@ from .bot import ( DocumentationLinkViewSet, InfractionViewSet, NominationViewSet, + OffensiveMessageViewSet, OffTopicChannelNameViewSet, ReminderViewSet, RoleViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index f1851e32..b3e0fa4d 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -5,6 +5,7 @@ from .documentation_link import DocumentationLinkViewSet from .infraction import InfractionViewSet from .nomination import NominationViewSet from .off_topic_channel_name import OffTopicChannelNameViewSet +from .offensive_message import OffensiveMessageViewSet from .reminder import ReminderViewSet from .role import RoleViewSet from .tag import TagViewSet diff --git a/pydis_site/apps/api/viewsets/bot/offensive_message.py b/pydis_site/apps/api/viewsets/bot/offensive_message.py new file mode 100644 index 00000000..54cb3a38 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/offensive_message.py @@ -0,0 +1,61 @@ +from rest_framework.mixins import ( + CreateModelMixin, + DestroyModelMixin, + ListModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.offensive_message import OffensiveMessage +from pydis_site.apps.api.serializers import OffensiveMessageSerializer + + +class OffensiveMessageViewSet( + CreateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet +): + """ + View providing CRUD access to offensive messages. + + ## Routes + ### GET /bot/offensive-messages + Returns all offensive messages in the database. + + #### Response format + >>> [ + ... { + ... 'id': '631953598091100200', + ... 'channel_id': '291284109232308226', + ... 'delete_date': '2019-11-01T21:51:15.545000Z' + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + + ### POST /bot/offensive-messages + Create a new offensive message object. + + #### Request body + >>> { + ... 'id': int, + ... 'channel_id': int, + ... 'delete_date': datetime.datetime # ISO-8601-formatted date + ... } + + #### Status codes + - 201: returned on success + - 400: if the body format is invalid + + ### DELETE /bot/offensive-messages/<id:int> + Delete the offensive message object with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a offensive message object with the given `id` does not exist + + ## Authentication + Requires an API token. + """ + + serializer_class = OffensiveMessageSerializer + queryset = OffensiveMessage.objects.all() diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index 32cb6bbf..1415c558 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -37,6 +37,7 @@ class TestLogsView(TestCase): channel_id=1984, content='<em>I think my tape has run out...</em>', embeds=[], + attachments=[], deletion_context=cls.deletion_context, ) @@ -101,6 +102,7 @@ class TestLogsView(TestCase): channel_id=1984, content='Does that mean this thing will halt?', embeds=[cls.embed_one, cls.embed_two], + attachments=['https://http.cat/100', 'https://http.cat/402'], deletion_context=cls.deletion_context, ) @@ -149,6 +151,21 @@ class TestLogsView(TestCase): self.assertInHTML(embed_colour_needle.format(colour=embed_one_colour), html_response) self.assertInHTML(embed_colour_needle.format(colour=embed_two_colour), html_response) + def test_if_both_attachments_are_included_html_response(self): + url = reverse('logs', host="staff", args=(self.deletion_context.id,)) + response = self.client.get(url) + + html_response = response.content.decode() + attachment_needle = '<img alt="Attachment" class="discord-attachment" src="{url}">' + self.assertInHTML( + attachment_needle.format(url=self.deleted_message_two.attachments[0]), + html_response + ) + self.assertInHTML( + attachment_needle.format(url=self.deleted_message_two.attachments[1]), + html_response + ) + def test_if_html_in_content_is_properly_escaped(self): url = reverse('logs', host="staff", args=(self.deletion_context.id,)) response = self.client.get(url) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 66376c4e..65ef2463 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -360,24 +360,7 @@ WIKI_MESSAGE_TAG_CSS_CLASS = { messages.WARNING: "is-warning", } -WIKI_MARKDOWN_HTML_STYLES = [ - 'max-width', - 'min-width', - 'margin', - 'padding', - 'width', - 'height', -] - -WIKI_MARKDOWN_HTML_ATTRIBUTES = { - 'img': ['class', 'id', 'src', 'alt', 'width', 'height'], - 'section': ['class', 'id'], - 'article': ['class', 'id'], -} - -WIKI_MARKDOWN_HTML_WHITELIST = [ - 'article', 'section', 'button' -] +WIKI_MARKDOWN_SANITIZE_HTML = False # Wiki permissions diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index dfcc6715..3b150767 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -37,11 +37,11 @@ </p> </div> - {# Intro video #} + {# Code Jam banner #} <div class="column is-half-desktop video-container"> - <iframe src="https://www.youtube.com/embed/DIBXg8Qh7bA" frameborder="0" - allow="accelerometer; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen></iframe> + <a href="https://pythondiscord.com/pages/code-jams/code-jam-6/"> + <img src="https://raw.githubusercontent.com/python-discord/branding/master/logos/logo_discord_banner/code%20jam%206%20-%20website%20banner.png"/> + </a> </div> </div> diff --git a/pydis_site/templates/staff/logs.html b/pydis_site/templates/staff/logs.html index 9c8ed7d3..a0bfa2a7 100644 --- a/pydis_site/templates/staff/logs.html +++ b/pydis_site/templates/staff/logs.html @@ -24,6 +24,11 @@ <div class="discord-message-content"> {{ message.content|linebreaks }} </div> + <div class="discord-message-attachments"> + {% for attachment in message.attachments %} + <img alt="Attachment" class="discord-attachment" src="{{ attachment }}"> + {% endfor %} + </div> {% for embed in message.embeds %} <div class="discord-embed is-size-7"> <div class="discord-embed-color" style="background-color: {% if embed.color %}{{ embed.color | hex_colour }}{% else %}#cacbce{% endif %}"></div> diff --git a/pydis_site/utils/account.py b/pydis_site/utils/account.py index e64919de..2d699c88 100644 --- a/pydis_site/utils/account.py +++ b/pydis_site/utils/account.py @@ -73,7 +73,7 @@ class SocialAccountAdapter(DefaultSocialAccountAdapter): """ if social_login.account.provider == "discord": discriminator = social_login.account.extra_data["discriminator"] - data["username"] = f"{data['username']}#{discriminator}" + data["username"] = f"{data['username']}#{discriminator:0>4}" data["name"] = data["username"] return super().populate_user(request, social_login, data) |