diff options
Diffstat (limited to '')
| -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/models.py | 70 | ||||
| -rw-r--r-- | api/serializers.py | 19 | ||||
| -rw-r--r-- | api/tests/test_models.py | 48 | ||||
| -rw-r--r-- | api/urls.py | 12 | ||||
| -rw-r--r-- | api/viewsets.py | 134 | 
9 files changed, 345 insertions, 15 deletions
| diff --git a/api/admin.py b/api/admin.py index c98f24eb..bcd41a7e 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,7 +1,8 @@  from django.contrib import admin  from .models import ( -    DocumentationLink, +    DeletedMessage, DocumentationLink, +    MessageDeletionContext,      OffTopicChannelName, Role,      SnakeFact, SnakeIdiom,      SnakeName, SpecialSnake, @@ -9,7 +10,9 @@ from .models import (  ) +admin.site.register(DeletedMessage)  admin.site.register(DocumentationLink) +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/models.py b/api/models.py index 7623c86c..68833328 100644 --- a/api/models.py +++ b/api/models.py @@ -216,6 +216,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 Tag(ModelReprMixin, models.Model):      """A tag providing (hopefully) useful information.""" diff --git a/api/serializers.py b/api/serializers.py index ba6dfaaf..8091ac63 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, -    OffTopicChannelName, +    DeletedMessage, DocumentationLink, +    MessageDeletionContext, OffTopicChannelName,      Role, SnakeFact,      SnakeIdiom, SnakeName, -    SpecialSnake, Tag, User +    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 2e606801..968f003e 100644 --- a/api/tests/test_models.py +++ b/api/tests/test_models.py @@ -1,11 +1,15 @@ +from datetime import datetime +  from django.test import SimpleTestCase  from ..models import ( -    DocumentationLink, ModelReprMixin, -    OffTopicChannelName, Role, -    SnakeFact, SnakeIdiom, -    SnakeName, SpecialSnake, -    Tag, User +    DeletedMessage, DocumentationLink, +    Message, MessageDeletionContext, +    ModelReprMixin, OffTopicChannelName, +    Role, SnakeFact, +    SnakeIdiom, SnakeName, +    SpecialSnake, Tag, +    User  ) @@ -26,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=datetime.utcnow() +                ), +                embeds=[] +            ),              DocumentationLink(                  'test', 'http://example.com', 'http://example.com'              ), @@ -41,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=datetime.utcnow() +            ),              User(                  id=5, name='bob',                  discriminator=1, avatar_hash=None diff --git a/api/urls.py b/api/urls.py index 59853934..dca208d8 100644 --- a/api/urls.py +++ b/api/urls.py @@ -3,8 +3,8 @@ from rest_framework.routers import DefaultRouter  from .views import HealthcheckView  from .viewsets import ( -    DocumentationLinkViewSet, -    OffTopicChannelNameViewSet, +    DeletedMessageViewSet, DocumentationLinkViewSet, +    OffTopicChannelNameViewSet, RoleViewSet,      SnakeFactViewSet, SnakeIdiomViewSet,      SnakeNameViewSet, SpecialSnakeViewSet,      TagViewSet, UserViewSet @@ -14,6 +14,10 @@ from .viewsets import (  # 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  ) @@ -27,6 +31,10 @@ bot_router.register(      UserViewSet  )  bot_router.register( +    'roles', +    RoleViewSet +) +bot_router.register(      'snake-facts',      SnakeFactViewSet  ) diff --git a/api/viewsets.py b/api/viewsets.py index de5ddaf6..e406e8c3 100644 --- a/api/viewsets.py +++ b/api/viewsets.py @@ -10,21 +10,56 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet  from rest_framework_bulk import BulkCreateModelMixin  from .models import ( -    DocumentationLink, -    OffTopicChannelName, +    DocumentationLink, MessageDeletionContext, +    OffTopicChannelName, Role,      SnakeFact, SnakeIdiom,      SnakeName, SpecialSnake,      Tag, User  )  from .serializers import ( -    DocumentationLinkSerializer, -    OffTopicChannelNameSerializer, +    DocumentationLinkSerializer, MessageDeletionContextSerializer, +    OffTopicChannelNameSerializer, 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  ): @@ -178,6 +213,97 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):          return Response(serialized.data) +class RoleViewSet(ModelViewSet): +    """ +    View providing CRUD access to the roles on our server, used +    by the bot to keep a mirror of our server's roles on the site. + +    ## Routes +    ### GET /bot/roles +    Returns all roles in the database. + +    #### Response format +    >>> [ +    ...     { +    ...         'id': 267628507062992896, +    ...         'name': "Admins", +    ...         'colour': 1337, +    ...         'permissions': 8 +    ...     } +    ... ] + +    #### Status codes +    - 200: returned on success + +    ### GET /bot/roles/<snowflake:int> +    Gets a single role by ID. + +    #### Response format +    >>> { +    ...     'id': 267628507062992896, +    ...     'name': "Admins", +    ...     'colour': 1337, +    ...     'permissions': 8 +    ... } + +    #### Status codes +    - 200: returned on success +    - 404: if a role with the given `snowflake` could not be found + +    ### POST /bot/roles +    Adds a single new role. + +    #### Request body +    >>> { +    ...     'id': int, +    ...     'name': str, +    ...     'colour': int, +    ...     'permissions': int, +    ... } + +    #### Status codes +    - 201: returned on success +    - 400: if the body format is invalid + +    ### PUT /bot/roles/<snowflake:int> +    Update the role with the given `snowflake`. +    All fields in the request body are required. + +    #### Request body +    >>> { +    ...     'id': int, +    ...     'name': str, +    ...     'colour': int, +    ...     'permissions': int +    ... } + +    #### Status codes +    - 200: returned on success +    - 400: if the request body was invalid + +    ### PATCH /bot/roles/<snowflake:int> +    Update the role with the given `snowflake`. +    All fields in the request body are required. + +    >>> { +    ...     'id': int, +    ...     'name': str, +    ...     'colour': int, +    ...     'permissions': int +    ... } + +    ### DELETE /bot/roles/<snowflake:int> +    Deletes the role with the given `snowflake`. + +    #### Status codes +    - 204: returned on success +    - 404: if a role with the given `snowflake` does not exist +    """ + +    queryset = Role.objects.all() +    serializer_class = RoleSerializer + +  class SnakeFactViewSet(ListModelMixin, GenericViewSet):      """      View providing snake facts created by the Pydis community in the first code jam. | 
