aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/admin.py5
-rw-r--r--api/migrations/0018_messagedeletioncontext.py24
-rw-r--r--api/migrations/0019_deletedmessage.py34
-rw-r--r--api/migrations/0021_merge_20181125_1015.py14
-rw-r--r--api/models.py70
-rw-r--r--api/serializers.py19
-rw-r--r--api/tests/test_models.py48
-rw-r--r--api/urls.py12
-rw-r--r--api/viewsets.py134
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.