diff options
-rw-r--r-- | api/admin.py | 5 | ||||
-rw-r--r-- | api/migrations/0018_messagedeletioncontext.py | 24 | ||||
-rw-r--r-- | api/migrations/0019_deletedmessage.py | 34 | ||||
-rw-r--r-- | api/migrations/0021_merge_20181125_1015.py | 14 | ||||
-rw-r--r-- | api/migrations/0027_merge_20190120_0852.py | 14 | ||||
-rw-r--r-- | api/models.py | 70 | ||||
-rw-r--r-- | api/serializers.py | 23 | ||||
-rw-r--r-- | api/tests/test_models.py | 42 | ||||
-rw-r--r-- | api/urls.py | 19 | ||||
-rw-r--r-- | api/viewsets.py | 52 |
10 files changed, 275 insertions, 22 deletions
diff --git a/api/admin.py b/api/admin.py index 2c8c130b..ab7e814e 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin from .models import ( - DocumentationLink, Infraction, + DeletedMessage, DocumentationLink, + Infraction, MessageDeletionContext, OffTopicChannelName, Role, SnakeFact, SnakeIdiom, SnakeName, SpecialSnake, @@ -9,8 +10,10 @@ from .models import ( ) +admin.site.register(DeletedMessage) admin.site.register(DocumentationLink) admin.site.register(Infraction) +admin.site.register(MessageDeletionContext) admin.site.register(OffTopicChannelName) admin.site.register(Role) admin.site.register(SnakeFact) diff --git a/api/migrations/0018_messagedeletioncontext.py b/api/migrations/0018_messagedeletioncontext.py new file mode 100644 index 00000000..88cbab28 --- /dev/null +++ b/api/migrations/0018_messagedeletioncontext.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.1 on 2018-11-18 20:12 + +import api.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0017_auto_20181029_1921'), + ] + + operations = [ + migrations.CreateModel( + name='MessageDeletionContext', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('creation', models.DateTimeField(help_text='When this deletion took place.')), + ('actor', models.ForeignKey(help_text='The original actor causing this deletion. Could be the author of a manual clean command invocation, the bot when executing automatic actions, or nothing to indicate that the bulk deletion was not issued by us.', null=True, on_delete=django.db.models.deletion.CASCADE, to='api.User')), + ], + bases=(api.models.ModelReprMixin, models.Model), + ), + ] diff --git a/api/migrations/0019_deletedmessage.py b/api/migrations/0019_deletedmessage.py new file mode 100644 index 00000000..fbd94949 --- /dev/null +++ b/api/migrations/0019_deletedmessage.py @@ -0,0 +1,34 @@ +# Generated by Django 2.1.1 on 2018-11-18 20:26 + +import api.models +import api.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0018_messagedeletioncontext'), + ] + + operations = [ + migrations.CreateModel( + name='DeletedMessage', + 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 this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), + ('content', models.CharField(help_text='The content of this message, taken from Discord.', max_length=2000)), + ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[api.validators.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)), + ('author', models.ForeignKey(help_text='The author of this message.', on_delete=django.db.models.deletion.CASCADE, to='api.User')), + ('deletion_context', models.ForeignKey(help_text='The deletion context this message is part of.', on_delete=django.db.models.deletion.CASCADE, to='api.MessageDeletionContext')), + ], + options={ + 'abstract': False, + }, + bases=(api.models.ModelReprMixin, models.Model), + ), + ] diff --git a/api/migrations/0021_merge_20181125_1015.py b/api/migrations/0021_merge_20181125_1015.py new file mode 100644 index 00000000..d8eaa510 --- /dev/null +++ b/api/migrations/0021_merge_20181125_1015.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.1 on 2018-11-25 10:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0020_add_snake_field_validators'), + ('api', '0019_deletedmessage'), + ] + + operations = [ + ] diff --git a/api/migrations/0027_merge_20190120_0852.py b/api/migrations/0027_merge_20190120_0852.py new file mode 100644 index 00000000..6fab4fd0 --- /dev/null +++ b/api/migrations/0027_merge_20190120_0852.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.5 on 2019-01-20 08:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0026_use_proper_default_for_infraction_insertion_date'), + ('api', '0021_merge_20181125_1015'), + ] + + operations = [ + ] diff --git a/api/models.py b/api/models.py index 092c2e8e..b063b78c 100644 --- a/api/models.py +++ b/api/models.py @@ -238,6 +238,76 @@ class User(ModelReprMixin, models.Model): return f"{self.name}#{self.discriminator}" +class Message(ModelReprMixin, models.Model): + 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." + ), + ) + ) + author = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The author of this message." + ) + channel_id = models.BigIntegerField( + help_text=( + "The channel ID that this message was " + "sent in, taken from Discord." + ), + validators=( + MinValueValidator( + limit_value=0, + message="Channel IDs cannot be negative." + ), + ) + ) + content = models.CharField( + max_length=2_000, + help_text="The content of this message, taken from Discord." + ) + embeds = pgfields.ArrayField( + pgfields.JSONField( + validators=(validate_tag_embed,) + ), + help_text="Embeds attached to this message." + ) + + class Meta: + abstract = True + + +class MessageDeletionContext(ModelReprMixin, models.Model): + actor = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text=( + "The original actor causing this deletion. Could be the author " + "of a manual clean command invocation, the bot when executing " + "automatic actions, or nothing to indicate that the bulk " + "deletion was not issued by us." + ), + null=True + ) + creation = models.DateTimeField( + # Consider whether we want to add a validator here that ensures + # the deletion context does not take place in the future. + help_text="When this deletion took place." + ) + + +class DeletedMessage(Message): + deletion_context = models.ForeignKey( + MessageDeletionContext, + help_text="The deletion context this message is part of.", + on_delete=models.CASCADE + ) + + class Infraction(ModelReprMixin, models.Model): """An infraction for a Discord user.""" diff --git a/api/serializers.py b/api/serializers.py index 612ce5b4..764f85e5 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -2,14 +2,27 @@ from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, from rest_framework_bulk import BulkSerializerMixin from .models import ( - DocumentationLink, Infraction, - OffTopicChannelName, - Role, SnakeFact, - SnakeIdiom, SnakeName, - SpecialSnake, Tag, User + DeletedMessage, DocumentationLink, + Infraction, MessageDeletionContext, + OffTopicChannelName, Role, + SnakeFact, SnakeIdiom, + SnakeName, SpecialSnake, + Tag, User ) +class MessageDeletionContextSerializer(BulkSerializerMixin, ModelSerializer): + deleted_messages = PrimaryKeyRelatedField( + many=True, + queryset=DeletedMessage.objects.all() + ) + + class Meta: + model = MessageDeletionContext + fields = ('actor', 'creation', 'messages') + depth = 1 + + class DocumentationLinkSerializer(ModelSerializer): class Meta: model = DocumentationLink diff --git a/api/tests/test_models.py b/api/tests/test_models.py index 1419a7d7..1fe60c0d 100644 --- a/api/tests/test_models.py +++ b/api/tests/test_models.py @@ -3,9 +3,11 @@ from datetime import datetime as dt, timezone from django.test import SimpleTestCase from ..models import ( - DocumentationLink, Infraction, - ModelReprMixin, OffTopicChannelName, - Role, SnakeFact, SnakeIdiom, + DeletedMessage, DocumentationLink, + Infraction, Message, + MessageDeletionContext, ModelReprMixin, + OffTopicChannelName, Role, + SnakeFact, SnakeIdiom, SnakeName, SpecialSnake, Tag, User ) @@ -28,6 +30,23 @@ class ReprMixinTests(SimpleTestCase): class StringDunderMethodTests(SimpleTestCase): def setUp(self): self.objects = ( + DeletedMessage( + id=45, + author=User( + id=444, name='bill', + discriminator=5, avatar_hash=None + ), + channel_id=666, + content="wooey", + deletion_context=MessageDeletionContext( + actor=User( + id=5555, name='shawn', + discriminator=555, avatar_hash=None + ), + creation=dt.utcnow() + ), + embeds=[] + ), DocumentationLink( 'test', 'http://example.com', 'http://example.com' ), @@ -43,6 +62,23 @@ class StringDunderMethodTests(SimpleTestCase): id=5, name='test role', colour=0x5, permissions=0 ), + Message( + id=45, + author=User( + id=444, name='bill', + discriminator=5, avatar_hash=None + ), + channel_id=666, + content="wooey", + embeds=[] + ), + MessageDeletionContext( + actor=User( + id=5555, name='shawn', + discriminator=555, avatar_hash=None + ), + creation=dt.utcnow() + ), Tag( title='bob', embed={'content': "the builder"} diff --git a/api/urls.py b/api/urls.py index 66d3fb9e..75980d75 100644 --- a/api/urls.py +++ b/api/urls.py @@ -3,17 +3,22 @@ from rest_framework.routers import DefaultRouter from .views import HealthcheckView, RulesView from .viewsets import ( - DocumentationLinkViewSet, InfractionViewSet, - OffTopicChannelNameViewSet, RoleViewSet, - SnakeFactViewSet, SnakeIdiomViewSet, - SnakeNameViewSet, SpecialSnakeViewSet, - TagViewSet, UserViewSet + DeletedMessageViewSet, DocumentationLinkViewSet, + InfractionViewSet, OffTopicChannelNameViewSet, + RoleViewSet, SnakeFactViewSet, + SnakeIdiomViewSet, SnakeNameViewSet, + SpecialSnakeViewSet, TagViewSet, + UserViewSet ) # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter bot_router = DefaultRouter(trailing_slash=False) bot_router.register( + 'deleted-messages', + DeletedMessageViewSet +) +bot_router.register( 'documentation-links', DocumentationLinkViewSet ) @@ -31,6 +36,10 @@ bot_router.register( RoleViewSet ) bot_router.register( + 'roles', + RoleViewSet +) +bot_router.register( 'snake-facts', SnakeFactViewSet ) diff --git a/api/viewsets.py b/api/viewsets.py index ab62f8a7..260d4b8a 100644 --- a/api/viewsets.py +++ b/api/viewsets.py @@ -14,21 +14,57 @@ from rest_framework_bulk import BulkCreateModelMixin from .models import ( DocumentationLink, Infraction, - OffTopicChannelName, Role, - SnakeFact, SnakeIdiom, - SnakeName, SpecialSnake, - Tag, User + MessageDeletionContext, OffTopicChannelName, + Role, SnakeFact, + SnakeIdiom, SnakeName, + SpecialSnake, Tag, + User ) from .serializers import ( DocumentationLinkSerializer, ExpandedInfractionSerializer, InfractionSerializer, OffTopicChannelNameSerializer, - RoleSerializer, SnakeFactSerializer, - SnakeIdiomSerializer, SnakeNameSerializer, - SpecialSnakeSerializer, TagSerializer, - UserSerializer + MessageDeletionContextSerializer, RoleSerializer, + SnakeFactSerializer, SnakeIdiomSerializer, + SnakeNameSerializer, SpecialSnakeSerializer, + TagSerializer, UserSerializer ) +class DeletedMessageViewSet(GenericViewSet): + """ + View providing support for posting bulk deletion logs generated by the bot. + + ## Routes + ### POST /bot/deleted-messages + Post messages from bulk deletion logs. + + #### Body schema + >>> { + ... # The member ID of the original actor, if applicable. + ... # If a member ID is given, it must be present on the site. + ... 'actor': Optional[int] + ... 'creation': datetime, + ... 'messages': [ + ... { + ... 'id': int, + ... 'author': int, + ... 'channel_id': int, + ... 'content': str, + ... 'embeds': [ + ... # Discord embed objects + ... ] + ... } + ... ] + ... } + + #### Status codes + - 204: returned on success + """ + + queryset = MessageDeletionContext.objects.all() + serializer = MessageDeletionContextSerializer + + class DocumentationLinkViewSet( CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet ): |