aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Johannes Christ <[email protected]>2019-04-17 21:39:12 +0200
committerGravatar Johannes Christ <[email protected]>2019-04-17 21:39:12 +0200
commit7c6ee82ed6f90121c163f7f684a289078933e2e3 (patch)
tree55e25f30dc89bc6afb464ca2566e034c0c2e9745
parentMerge pull request #207 from python-discord/api/split-models (diff)
Restructure viewsets to submodules.
-rw-r--r--pydis_site/apps/api/models/bot/message.py2
-rw-r--r--pydis_site/apps/api/viewsets.py923
-rw-r--r--pydis_site/apps/api/viewsets/__init__.py17
-rw-r--r--pydis_site/apps/api/viewsets/bot/__init__.py14
-rw-r--r--pydis_site/apps/api/viewsets/bot/bot_setting.py14
-rw-r--r--pydis_site/apps/api/viewsets/bot/deleted_message.py40
-rw-r--r--pydis_site/apps/api/viewsets/bot/documentation_link.py72
-rw-r--r--pydis_site/apps/api/viewsets/bot/infraction.py155
-rw-r--r--pydis_site/apps/api/viewsets/bot/nomination.py25
-rw-r--r--pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py98
-rw-r--r--pydis_site/apps/api/viewsets/bot/reminder.py66
-rw-r--r--pydis_site/apps/api/viewsets/bot/role.py95
-rw-r--r--pydis_site/apps/api/viewsets/bot/snake_fact.py30
-rw-r--r--pydis_site/apps/api/viewsets/bot/snake_idiom.py30
-rw-r--r--pydis_site/apps/api/viewsets/bot/snake_name.py61
-rw-r--r--pydis_site/apps/api/viewsets/bot/special_snake.py33
-rw-r--r--pydis_site/apps/api/viewsets/bot/tag.py105
-rw-r--r--pydis_site/apps/api/viewsets/bot/user.py126
-rw-r--r--pydis_site/apps/api/viewsets/log_entry.py37
19 files changed, 1019 insertions, 924 deletions
diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py
index 22500be0..2578d5c6 100644
--- a/pydis_site/apps/api/models/bot/message.py
+++ b/pydis_site/apps/api/models/bot/message.py
@@ -2,9 +2,9 @@ from django.contrib.postgres import fields as pgfields
from django.core.validators import MinValueValidator
from django.db import models
-from pydis_site.apps.api.models.utils import ModelReprMixin
from pydis_site.apps.api.models.bot.tag import validate_tag_embed
from pydis_site.apps.api.models.bot.user import User
+from pydis_site.apps.api.models.utils import ModelReprMixin
class Message(ModelReprMixin, models.Model):
diff --git a/pydis_site/apps/api/viewsets.py b/pydis_site/apps/api/viewsets.py
deleted file mode 100644
index 47915256..00000000
--- a/pydis_site/apps/api/viewsets.py
+++ /dev/null
@@ -1,923 +0,0 @@
-from django.shortcuts import get_object_or_404
-from django_filters.rest_framework import DjangoFilterBackend
-from rest_framework.decorators import action
-from rest_framework.exceptions import ParseError, ValidationError
-from rest_framework.filters import SearchFilter
-from rest_framework.mixins import (
- CreateModelMixin, DestroyModelMixin,
- ListModelMixin, RetrieveModelMixin,
- UpdateModelMixin
-)
-from rest_framework.response import Response
-from rest_framework.status import HTTP_201_CREATED
-from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
-from rest_framework_bulk import BulkCreateModelMixin
-
-from .models import (
- BotSetting, DocumentationLink,
- Infraction, LogEntry,
- MessageDeletionContext, Nomination,
- OffTopicChannelName, Reminder,
- Role, SnakeFact,
- SnakeIdiom, SnakeName,
- SpecialSnake, Tag,
- User
-)
-from .serializers import (
- BotSettingSerializer, DocumentationLinkSerializer,
- ExpandedInfractionSerializer, InfractionSerializer,
- LogEntrySerializer, MessageDeletionContextSerializer,
- NominationSerializer, OffTopicChannelNameSerializer,
- ReminderSerializer, RoleSerializer,
- SnakeFactSerializer, SnakeIdiomSerializer,
- SnakeNameSerializer, SpecialSnakeSerializer,
- TagSerializer, UserSerializer
-)
-
-
-class BotSettingViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
- """
- View providing update operations on bot setting routes.
- """
-
- serializer_class = BotSettingSerializer
- queryset = BotSetting.objects.all()
-
-
-class DeletedMessageViewSet(CreateModelMixin, 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_class = MessageDeletionContextSerializer
-
-
-class DocumentationLinkViewSet(
- CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet
-):
- """
- View providing management of documentation links used in the bot's `Doc` cog.
-
- ## Routes
- ### GET /bot/documentation-links
- Retrieve all currently stored entries from the database.
-
- #### Response format
- >>> [
- ... {
- ... 'package': 'flask',
- ... 'base_url': 'https://flask.pocoo.org/docs/dev',
- ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv'
- ... },
- ... # ...
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ### GET /bot/documentation-links/<package:str>
- Look up the documentation object for the given `package`.
-
- #### Response format
- >>> {
- ... 'package': 'flask',
- ... 'base_url': 'https://flask.pocoo.org/docs/dev',
- ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv'
- ... }
-
- #### Status codes
- - 200: returned on success
- - 404: if no entry for the given `package` exists
-
- ### POST /bot/documentation-links
- Create a new documentation link object.
-
- #### Body schema
- >>> {
- ... 'package': str,
- ... 'base_url': URL,
- ... 'inventory_url': URL
- ... }
-
- #### Status codes
- - 201: returned on success
- - 400: if the request body has invalid fields, see the response for details
-
- ### DELETE /bot/documentation-links/<package:str>
- Delete the entry for the given `package`.
-
- #### Status codes
- - 204: returned on success
- - 404: if the given `package` could not be found
- """
-
- queryset = DocumentationLink.objects.all()
- serializer_class = DocumentationLinkSerializer
- lookup_field = 'package'
-
-
-class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet):
- """
- View providing CRUD operations on infractions for Discord users.
-
- ## Routes
- ### GET /bot/infractions
- Retrieve all infractions.
- May be filtered by the query parameters.
-
- #### Query parameters
- - **active** `bool`: whether the infraction is still active
- - **actor** `int`: snowflake of the user which applied the infraction
- - **hidden** `bool`: whether the infraction is a shadow infraction
- - **search** `str`: regular expression applied to the infraction's reason
- - **type** `str`: the type of the infraction
- - **user** `int`: snowflake of the user to which the infraction was applied
-
- Invalid query parameters are ignored.
-
- #### Response format
- >>> [
- ... {
- ... 'id': 5,
- ... 'inserted_at': '2018-11-22T07:24:06.132307Z',
- ... 'expires_at': '5018-11-20T15:52:00Z',
- ... 'active': False,
- ... 'user': 172395097705414656,
- ... 'actor': 125435062127820800,
- ... 'type': 'ban',
- ... 'reason': 'He terk my jerb!',
- ... 'hidden': True
- ... }
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ### GET /bot/infractions/<id:int>
- Retrieve a single infraction by ID.
-
- #### Response format
- See `GET /bot/infractions`.
-
- #### Status codes
- - 200: returned on success
- - 404: if an infraction with the given `id` could not be found
-
- ### POST /bot/infractions
- Create a new infraction and return the created infraction.
- Only `actor`, `type`, and `user` are required.
- The `actor` and `user` must be users known by the site.
-
- #### Request body
- >>> {
- ... 'active': False,
- ... 'actor': 125435062127820800,
- ... 'expires_at': '5018-11-20T15:52:00+00:00',
- ... 'hidden': True,
- ... 'type': 'ban',
- ... 'reason': 'He terk my jerb!',
- ... 'user': 172395097705414656
- ... }
-
- #### Response format
- See `GET /bot/infractions`.
-
- #### Status codes
- - 201: returned on success
- - 400: if a given user is unknown or a field in the request body is invalid
-
- ### PATCH /bot/infractions/<id:int>
- Update the infraction with the given `id` and return the updated infraction.
- Only `active`, `reason`, and `expires_at` may be updated.
-
- #### Request body
- >>> {
- ... 'active': True,
- ... 'expires_at': '4143-02-15T21:04:31+00:00',
- ... 'reason': 'durka derr'
- ... }
-
- #### Response format
- See `GET /bot/infractions`.
-
- #### Status codes
- - 200: returned on success
- - 400: if a field in the request body is invalid or disallowed
- - 404: if an infraction with the given `id` could not be found
-
- ### Expanded routes
- All routes support expansion of `user` and `actor` in responses. To use an expanded route,
- append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`.
-
- #### Response format
- See `GET /bot/users/<snowflake:int>` for the expanded formats of `user` and `actor`. Responses
- are otherwise identical to their non-expanded counterparts.
- """
-
- serializer_class = InfractionSerializer
- queryset = Infraction.objects.all()
- filter_backends = (DjangoFilterBackend, SearchFilter)
- filter_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type')
- search_fields = ('$reason',)
- frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden')
-
- def partial_update(self, request, *args, **kwargs):
- for field in request.data:
- if field in self.frozen_fields:
- raise ValidationError({field: ['This field cannot be updated.']})
-
- instance = self.get_object()
- serializer = self.get_serializer(instance, data=request.data, partial=True)
- serializer.is_valid(raise_exception=True)
- serializer.save()
-
- return Response(serializer.data)
-
- @action(url_path='expanded', detail=False)
- def list_expanded(self, *args, **kwargs):
- self.serializer_class = ExpandedInfractionSerializer
- return self.list(*args, **kwargs)
-
- @list_expanded.mapping.post
- def create_expanded(self, *args, **kwargs):
- self.serializer_class = ExpandedInfractionSerializer
- return self.create(*args, **kwargs)
-
- @action(url_path='expanded', url_name='detail-expanded', detail=True)
- def retrieve_expanded(self, *args, **kwargs):
- self.serializer_class = ExpandedInfractionSerializer
- return self.retrieve(*args, **kwargs)
-
- @retrieve_expanded.mapping.patch
- def partial_update_expanded(self, *args, **kwargs):
- self.serializer_class = ExpandedInfractionSerializer
- return self.partial_update(*args, **kwargs)
-
-
-class LogEntryViewSet(CreateModelMixin, GenericViewSet):
- """
- View providing support for creating log entries in the site database
- for viewing via the log browser.
-
- ## Routes
- ### POST /logs
- Create a new log entry.
-
- #### Request body
- >>> {
- ... 'application': str, # 'bot' | 'seasonalbot' | 'site'
- ... 'logger_name': str, # such as 'bot.cogs.moderation'
- ... 'timestamp': Optional[str], # from `datetime.utcnow().isoformat()`
- ... 'level': str, # 'debug' | 'info' | 'warning' | 'error' | 'critical'
- ... 'module': str, # such as 'pydis_site.apps.api.serializers'
- ... 'line': int, # > 0
- ... 'message': str, # textual formatted content of the logline
- ... }
-
- #### Status codes
- - 201: returned on success
- - 400: if the request body has invalid fields, see the response for details
-
- ## Authentication
- Requires a API token.
- """
-
- queryset = LogEntry.objects.all()
- serializer_class = LogEntrySerializer
-
-
-class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
- """
- View of off-topic channel names used by the bot
- to rotate our off-topic names on a daily basis.
-
- ## Routes
- ### GET /bot/off-topic-channel-names
- Return all known off-topic channel names from the database.
- If the `random_items` query parameter is given, for example using...
- $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5
- ... then the API will return `5` random items from the database.
-
- #### Response format
- Return a list of off-topic-channel names:
- >>> [
- ... "lemons-lemonade-stand",
- ... "bbq-with-bisk"
- ... ]
-
- #### Status codes
- - 200: returned on success
- - 400: returned when `random_items` is not a positive integer
-
- ### POST /bot/off-topic-channel-names
- Create a new off-topic-channel name in the database.
- The name must be given as a query parameter, for example:
- $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?name=lemons-lemonade-shop
-
- #### Status codes
- - 201: returned on success
- - 400: if the request body has invalid fields, see the response for details
-
- ### DELETE /bot/off-topic-channel-names/<name:str>
- Delete the off-topic-channel name with the given `name`.
-
- #### Status codes
- - 204: returned on success
- - 404: returned when the given `name` was not found
-
- ## Authentication
- Requires a API token.
- """
-
- lookup_field = 'name'
- serializer_class = OffTopicChannelNameSerializer
-
- def get_object(self):
- queryset = self.get_queryset()
- name = self.kwargs[self.lookup_field]
- return get_object_or_404(queryset, name=name)
-
- def get_queryset(self):
- return OffTopicChannelName.objects.all()
-
- def create(self, request):
- if 'name' in request.query_params:
- create_data = {'name': request.query_params['name']}
- serializer = OffTopicChannelNameSerializer(data=create_data)
- serializer.is_valid(raise_exception=True)
- serializer.save()
- return Response(create_data, status=HTTP_201_CREATED)
-
- else:
- raise ParseError(detail={
- 'name': ["This query parameter is required."]
- })
-
- def list(self, request): # noqa
- if 'random_items' in request.query_params:
- param = request.query_params['random_items']
- try:
- random_count = int(param)
- except ValueError:
- raise ParseError(detail={'random_items': ["Must be a valid integer."]})
-
- if random_count <= 0:
- raise ParseError(detail={
- 'random_items': ["Must be a positive integer."]
- })
-
- queryset = self.get_queryset().order_by('?')[:random_count]
- serialized = self.serializer_class(queryset, many=True)
- return Response(serialized.data)
-
- queryset = self.get_queryset()
- serialized = self.serializer_class(queryset, many=True)
- return Response(serialized.data)
-
-
-class ReminderViewSet(CreateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet):
- """
- View providing CRUD access to reminders.
-
- ## Routes
- ### GET /bot/reminders
- Returns all reminders in the database.
-
- #### Response format
- >>> [
- ... {
- ... 'active': True,
- ... 'author': 1020103901030,
- ... 'content': "Make dinner",
- ... 'expiration': '5018-11-20T15:52:00Z',
- ... 'id': 11
- ... },
- ... ...
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ### POST /bot/reminders
- Create a new reminder.
-
- #### Request body
- >>> {
- ... 'author': int,
- ... 'content': str,
- ... 'expiration': str # ISO-formatted datetime
- ... }
-
- #### Status codes
- - 201: returned on success
- - 400: if the body format is invalid
- - 404: if no user with the given ID could be found
-
- ### DELETE /bot/reminders/<id:int>
- Delete the reminder with the given `id`.
-
- #### Status codes
- - 204: returned on success
- - 404: if a reminder with the given `id` does not exist
-
- ## Authentication
- Requires an API token.
- """
-
- serializer_class = ReminderSerializer
- queryset = Reminder.objects.prefetch_related('author')
- filter_backends = (DjangoFilterBackend, SearchFilter)
- filter_fields = ('active', 'author__id')
-
-
-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.
-
- ## Routes
- ### GET /bot/snake-facts
- Returns snake facts from the database.
-
- #### Response format
- >>> [
- ... {'fact': 'Snakes are dangerous'},
- ... {'fact': 'Except for Python, we all love it'}
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ## Authentication
- Requires an API token.
- """
-
- serializer_class = SnakeFactSerializer
- queryset = SnakeFact.objects.all()
-
-
-class SnakeIdiomViewSet(ListModelMixin, GenericViewSet):
- """
- View providing snake idioms for the snake cog.
-
- ## Routes
- ### GET /bot/snake-idioms
- Returns snake idioms from the database.
-
- #### Response format
- >>> [
- ... {'idiom': 'Sneky snek'},
- ... {'idiom': 'Snooky Snake'}
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ## Authentication
- Requires an API token
- """
-
- serializer_class = SnakeIdiomSerializer
- queryset = SnakeIdiom.objects.all()
-
-
-class SnakeNameViewSet(ViewSet):
- """
- View providing snake names for the bot's snake cog from our first code jam's winners.
-
- ## Routes
- ### GET /bot/snake-names
- By default, return a single random snake name along with its name and scientific name.
- If the `get_all` query parameter is given, for example using...
- $ curl api.pythondiscord.local:8000/bot/snake-names?get_all=yes
- ... then the API will return all snake names and scientific names in the database.
-
- #### Response format
- Without `get_all` query parameter:
- >>> {
- ... 'name': "Python",
- ... 'scientific': "Langus greatus"
- ... }
-
- If the database is empty for whatever reason, this will return an empty dictionary.
-
- With `get_all` query parameter:
- >>> [
- ... {'name': "Python 3", 'scientific': "Langus greatus"},
- ... {'name': "Python 2", 'scientific': "Langus decentus"}
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ## Authentication
- Requires a API token.
- """
-
- serializer_class = SnakeNameSerializer
-
- def get_queryset(self):
- return SnakeName.objects.all()
-
- def list(self, request): # noqa
- if request.query_params.get('get_all'):
- queryset = self.get_queryset()
- serialized = self.serializer_class(queryset, many=True)
- return Response(serialized.data)
-
- single_snake = SnakeName.objects.order_by('?').first()
- if single_snake is not None:
- body = {
- 'name': single_snake.name,
- 'scientific': single_snake.scientific
- }
-
- return Response(body)
-
- return Response({})
-
-
-class SpecialSnakeViewSet(ListModelMixin, GenericViewSet):
- """
- View providing special snake names for our bot's snake cog.
-
- ## Routes
- ### GET /bot/special-snakes
- Returns a list of special snake names.
-
- #### Response Format
- >>> [
- ... {
- ... 'name': 'Snakky sneakatus',
- ... 'info': 'Scary snek',
- ... 'image': 'https://discordapp.com/assets/53ef346458017da2062aca5c7955946b.svg'
- ... }
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ## Authentication
- Requires an API token.
- """
-
- serializer_class = SpecialSnakeSerializer
- queryset = SpecialSnake.objects.all()
-
-
-class TagViewSet(ModelViewSet):
- """
- View providing CRUD operations on tags shown by our bot.
-
- ## Routes
- ### GET /bot/tags
- Returns all tags in the database.
-
- #### Response format
- >>> [
- ... {
- ... 'title': "resources",
- ... 'embed': {
- ... 'content': "Did you really think I'd put something useful here?"
- ... }
- ... }
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ### GET /bot/tags/<title:str>
- Gets a single tag by its title.
-
- #### Response format
- >>> {
- ... 'title': "My awesome tag",
- ... 'embed': {
- ... 'content': "totally not filler words"
- ... }
- ... }
-
- #### Status codes
- - 200: returned on success
- - 404: if a tag with the given `title` could not be found
-
- ### POST /bot/tags
- Adds a single tag to the database.
-
- #### Request body
- >>> {
- ... 'title': str,
- ... 'embed': dict
- ... }
-
- The embed structure is the same as the embed structure that the Discord API
- expects. You can view the documentation for it here:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- #### Status codes
- - 201: returned on success
- - 400: if one of the given fields is invalid
-
- ### PUT /bot/tags/<title:str>
- Update the tag with the given `title`.
-
- #### Request body
- >>> {
- ... 'title': str,
- ... 'embed': dict
- ... }
-
- The embed structure is the same as the embed structure that the Discord API
- expects. You can view the documentation for it here:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- #### Status codes
- - 200: returned on success
- - 400: if the request body was invalid, see response body for details
- - 404: if the tag with the given `title` could not be found
-
- ### PATCH /bot/tags/<title:str>
- Update the tag with the given `title`.
-
- #### Request body
- >>> {
- ... 'title': str,
- ... 'embed': dict
- ... }
-
- The embed structure is the same as the embed structure that the Discord API
- expects. You can view the documentation for it here:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- #### Status codes
- - 200: returned on success
- - 400: if the request body was invalid, see response body for details
- - 404: if the tag with the given `title` could not be found
-
- ### DELETE /bot/tags/<title:str>
- Deletes the tag with the given `title`.
-
- #### Status codes
- - 204: returned on success
- - 404: if a tag with the given `title` does not exist
- """
-
- serializer_class = TagSerializer
- queryset = Tag.objects.all()
-
-
-class UserViewSet(BulkCreateModelMixin, ModelViewSet):
- """
- View providing CRUD operations on Discord users through the bot.
-
- ## Routes
- ### GET /bot/users
- Returns all users currently known.
-
- #### Response format
- >>> [
- ... {
- ... 'id': 409107086526644234,
- ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb",
- ... 'name': "Python",
- ... 'discriminator': 4329,
- ... 'roles': [
- ... 352427296948486144,
- ... 270988689419665409,
- ... 277546923144249364,
- ... 458226699344019457
- ... ],
- ... 'in_guild': True
- ... }
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ### GET /bot/users/<snowflake:int>
- Gets a single user by ID.
-
- #### Response format
- >>> {
- ... 'id': 409107086526644234,
- ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb",
- ... 'name': "Python",
- ... 'discriminator': 4329,
- ... 'roles': [
- ... 352427296948486144,
- ... 270988689419665409,
- ... 277546923144249364,
- ... 458226699344019457
- ... ],
- ... 'in_guild': True
- ... }
-
- #### Status codes
- - 200: returned on success
- - 404: if a user with the given `snowflake` could not be found
-
- ### POST /bot/users
- Adds a single or multiple new users.
- The roles attached to the user(s) must be roles known by the site.
-
- #### Request body
- >>> {
- ... 'id': int,
- ... 'avatar': str,
- ... 'name': str,
- ... 'discriminator': int,
- ... 'roles': List[int],
- ... 'in_guild': bool
- ... }
-
- Alternatively, request users can be POSTed as a list of above objects,
- in which case multiple users will be created at once.
-
- #### Status codes
- - 201: returned on success
- - 400: if one of the given roles does not exist, or one of the given fields is invalid
-
- ### PUT /bot/users/<snowflake:int>
- Update the user with the given `snowflake`.
- All fields in the request body are required.
-
- #### Request body
- >>> {
- ... 'id': int,
- ... 'avatar': str,
- ... 'name': str,
- ... 'discriminator': int,
- ... 'roles': List[int],
- ... 'in_guild': bool
- ... }
-
- #### Status codes
- - 200: returned on success
- - 400: if the request body was invalid, see response body for details
- - 404: if the user with the given `snowflake` could not be found
-
- ### PATCH /bot/users/<snowflake:int>
- Update the user with the given `snowflake`.
- All fields in the request body are optional.
-
- #### Request body
- >>> {
- ... 'id': int,
- ... 'avatar': str,
- ... 'name': str,
- ... 'discriminator': int,
- ... 'roles': List[int],
- ... 'in_guild': bool
- ... }
-
- #### Status codes
- - 200: returned on success
- - 400: if the request body was invalid, see response body for details
- - 404: if the user with the given `snowflake` could not be found
-
- ### DELETE /bot/users/<snowflake:int>
- Deletes the user with the given `snowflake`.
-
- #### Status codes
- - 204: returned on success
- - 404: if a user with the given `snowflake` does not exist
- """
-
- serializer_class = UserSerializer
- queryset = User.objects.prefetch_related('roles')
-
-
-class NominationViewSet(ModelViewSet):
- # TODO: doc me
- serializer_class = NominationSerializer
- queryset = Nomination.objects.prefetch_related('author', 'user')
- frozen_fields = ('author', 'inserted_at', 'user')
-
- def update(self, request, *args, **kwargs):
- for field in request.data:
- if field in self.frozen_fields:
- raise ValidationError({field: ['This field cannot be updated.']})
-
- instance = self.get_object()
- serializer = self.get_serializer(instance, data=request.data, partial=True)
- serializer.is_valid(raise_exception=True)
- serializer.save()
-
- return Response(serializer.data)
diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py
new file mode 100644
index 00000000..553ca2c3
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/__init__.py
@@ -0,0 +1,17 @@
+from .bot import ( # noqa
+ BotSettingViewSet,
+ DeletedMessageViewSet,
+ DocumentationLinkViewSet,
+ InfractionViewSet,
+ NominationViewSet,
+ OffTopicChannelNameViewSet,
+ ReminderViewSet,
+ RoleViewSet,
+ SnakeFactViewSet,
+ SnakeIdiomViewSet,
+ SnakeNameViewSet,
+ SpecialSnakeViewSet,
+ TagViewSet,
+ UserViewSet
+)
+from .log_entry import LogEntryViewSet # noqa
diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py
new file mode 100644
index 00000000..8e7d1290
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/__init__.py
@@ -0,0 +1,14 @@
+from .bot_setting import BotSettingViewSet # noqa
+from .deleted_message import DeletedMessageViewSet # noqa
+from .documentation_link import DocumentationLinkViewSet # noqa
+from .infraction import InfractionViewSet # noqa
+from .nomination import NominationViewSet # noqa
+from .off_topic_channel_name import OffTopicChannelNameViewSet # noqa
+from .reminder import ReminderViewSet # noqa
+from .role import RoleViewSet # noqa
+from .snake_fact import SnakeFactViewSet # noqa
+from .snake_idiom import SnakeIdiomViewSet # noqa
+from .snake_name import SnakeNameViewSet # noqa
+from .special_snake import SpecialSnakeViewSet # noqa
+from .tag import TagViewSet # noqa
+from .user import UserViewSet # noqa
diff --git a/pydis_site/apps/api/viewsets/bot/bot_setting.py b/pydis_site/apps/api/viewsets/bot/bot_setting.py
new file mode 100644
index 00000000..5464018a
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/bot_setting.py
@@ -0,0 +1,14 @@
+from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot.bot_setting import BotSetting
+from pydis_site.apps.api.serializers import BotSettingSerializer
+
+
+class BotSettingViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
+ """
+ View providing update operations on bot setting routes.
+ """
+
+ serializer_class = BotSettingSerializer
+ queryset = BotSetting.objects.all()
diff --git a/pydis_site/apps/api/viewsets/bot/deleted_message.py b/pydis_site/apps/api/viewsets/bot/deleted_message.py
new file mode 100644
index 00000000..c14171bd
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/deleted_message.py
@@ -0,0 +1,40 @@
+from rest_framework.mixins import CreateModelMixin
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot.message_deletion_context import MessageDeletionContext
+from pydis_site.apps.api.serializers import MessageDeletionContextSerializer
+
+
+class DeletedMessageViewSet(CreateModelMixin, 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_class = MessageDeletionContextSerializer
diff --git a/pydis_site/apps/api/viewsets/bot/documentation_link.py b/pydis_site/apps/api/viewsets/bot/documentation_link.py
new file mode 100644
index 00000000..6432d344
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/documentation_link.py
@@ -0,0 +1,72 @@
+from rest_framework.mixins import (
+ CreateModelMixin, DestroyModelMixin,
+ ListModelMixin, RetrieveModelMixin
+)
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot.documentation_link import DocumentationLink
+from pydis_site.apps.api.serializers import DocumentationLinkSerializer
+
+
+class DocumentationLinkViewSet(
+ CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet
+):
+ """
+ View providing management of documentation links used in the bot's `Doc` cog.
+
+ ## Routes
+ ### GET /bot/documentation-links
+ Retrieve all currently stored entries from the database.
+
+ #### Response format
+ >>> [
+ ... {
+ ... 'package': 'flask',
+ ... 'base_url': 'https://flask.pocoo.org/docs/dev',
+ ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv'
+ ... },
+ ... # ...
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+
+ ### GET /bot/documentation-links/<package:str>
+ Look up the documentation object for the given `package`.
+
+ #### Response format
+ >>> {
+ ... 'package': 'flask',
+ ... 'base_url': 'https://flask.pocoo.org/docs/dev',
+ ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv'
+ ... }
+
+ #### Status codes
+ - 200: returned on success
+ - 404: if no entry for the given `package` exists
+
+ ### POST /bot/documentation-links
+ Create a new documentation link object.
+
+ #### Body schema
+ >>> {
+ ... 'package': str,
+ ... 'base_url': URL,
+ ... 'inventory_url': URL
+ ... }
+
+ #### Status codes
+ - 201: returned on success
+ - 400: if the request body has invalid fields, see the response for details
+
+ ### DELETE /bot/documentation-links/<package:str>
+ Delete the entry for the given `package`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: if the given `package` could not be found
+ """
+
+ queryset = DocumentationLink.objects.all()
+ serializer_class = DocumentationLinkSerializer
+ lookup_field = 'package'
diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py
new file mode 100644
index 00000000..8eacf5c1
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/infraction.py
@@ -0,0 +1,155 @@
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.decorators import action
+from rest_framework.exceptions import ValidationError
+from rest_framework.filters import SearchFilter
+from rest_framework.mixins import (
+ CreateModelMixin,
+ ListModelMixin,
+ RetrieveModelMixin
+)
+from rest_framework.response import Response
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot.infraction import Infraction
+from pydis_site.apps.api.serializers import (
+ ExpandedInfractionSerializer,
+ InfractionSerializer
+)
+
+
+class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet):
+ """
+ View providing CRUD operations on infractions for Discord users.
+
+ ## Routes
+ ### GET /bot/infractions
+ Retrieve all infractions.
+ May be filtered by the query parameters.
+
+ #### Query parameters
+ - **active** `bool`: whether the infraction is still active
+ - **actor** `int`: snowflake of the user which applied the infraction
+ - **hidden** `bool`: whether the infraction is a shadow infraction
+ - **search** `str`: regular expression applied to the infraction's reason
+ - **type** `str`: the type of the infraction
+ - **user** `int`: snowflake of the user to which the infraction was applied
+
+ Invalid query parameters are ignored.
+
+ #### Response format
+ >>> [
+ ... {
+ ... 'id': 5,
+ ... 'inserted_at': '2018-11-22T07:24:06.132307Z',
+ ... 'expires_at': '5018-11-20T15:52:00Z',
+ ... 'active': False,
+ ... 'user': 172395097705414656,
+ ... 'actor': 125435062127820800,
+ ... 'type': 'ban',
+ ... 'reason': 'He terk my jerb!',
+ ... 'hidden': True
+ ... }
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+
+ ### GET /bot/infractions/<id:int>
+ Retrieve a single infraction by ID.
+
+ #### Response format
+ See `GET /bot/infractions`.
+
+ #### Status codes
+ - 200: returned on success
+ - 404: if an infraction with the given `id` could not be found
+
+ ### POST /bot/infractions
+ Create a new infraction and return the created infraction.
+ Only `actor`, `type`, and `user` are required.
+ The `actor` and `user` must be users known by the site.
+
+ #### Request body
+ >>> {
+ ... 'active': False,
+ ... 'actor': 125435062127820800,
+ ... 'expires_at': '5018-11-20T15:52:00+00:00',
+ ... 'hidden': True,
+ ... 'type': 'ban',
+ ... 'reason': 'He terk my jerb!',
+ ... 'user': 172395097705414656
+ ... }
+
+ #### Response format
+ See `GET /bot/infractions`.
+
+ #### Status codes
+ - 201: returned on success
+ - 400: if a given user is unknown or a field in the request body is invalid
+
+ ### PATCH /bot/infractions/<id:int>
+ Update the infraction with the given `id` and return the updated infraction.
+ Only `active`, `reason`, and `expires_at` may be updated.
+
+ #### Request body
+ >>> {
+ ... 'active': True,
+ ... 'expires_at': '4143-02-15T21:04:31+00:00',
+ ... 'reason': 'durka derr'
+ ... }
+
+ #### Response format
+ See `GET /bot/infractions`.
+
+ #### Status codes
+ - 200: returned on success
+ - 400: if a field in the request body is invalid or disallowed
+ - 404: if an infraction with the given `id` could not be found
+
+ ### Expanded routes
+ All routes support expansion of `user` and `actor` in responses. To use an expanded route,
+ append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`.
+
+ #### Response format
+ See `GET /bot/users/<snowflake:int>` for the expanded formats of `user` and `actor`. Responses
+ are otherwise identical to their non-expanded counterparts.
+ """
+
+ serializer_class = InfractionSerializer
+ queryset = Infraction.objects.all()
+ filter_backends = (DjangoFilterBackend, SearchFilter)
+ filter_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type')
+ search_fields = ('$reason',)
+ frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden')
+
+ def partial_update(self, request, *args, **kwargs):
+ for field in request.data:
+ if field in self.frozen_fields:
+ raise ValidationError({field: ['This field cannot be updated.']})
+
+ instance = self.get_object()
+ serializer = self.get_serializer(instance, data=request.data, partial=True)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+
+ return Response(serializer.data)
+
+ @action(url_path='expanded', detail=False)
+ def list_expanded(self, *args, **kwargs):
+ self.serializer_class = ExpandedInfractionSerializer
+ return self.list(*args, **kwargs)
+
+ @list_expanded.mapping.post
+ def create_expanded(self, *args, **kwargs):
+ self.serializer_class = ExpandedInfractionSerializer
+ return self.create(*args, **kwargs)
+
+ @action(url_path='expanded', url_name='detail-expanded', detail=True)
+ def retrieve_expanded(self, *args, **kwargs):
+ self.serializer_class = ExpandedInfractionSerializer
+ return self.retrieve(*args, **kwargs)
+
+ @retrieve_expanded.mapping.patch
+ def partial_update_expanded(self, *args, **kwargs):
+ self.serializer_class = ExpandedInfractionSerializer
+ return self.partial_update(*args, **kwargs)
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
new file mode 100644
index 00000000..725ae176
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -0,0 +1,25 @@
+from rest_framework.exceptions import ValidationError
+from rest_framework.response import Response
+from rest_framework.viewsets import ModelViewSet
+
+from pydis_site.apps.api.models.bot import Nomination
+from pydis_site.apps.api.serializers import NominationSerializer
+
+
+class NominationViewSet(ModelViewSet):
+ # TODO: doc me
+ serializer_class = NominationSerializer
+ queryset = Nomination.objects.prefetch_related('author', 'user')
+ frozen_fields = ('author', 'inserted_at', 'user')
+
+ def update(self, request, *args, **kwargs):
+ for field in request.data:
+ if field in self.frozen_fields:
+ raise ValidationError({field: ['This field cannot be updated.']})
+
+ instance = self.get_object()
+ serializer = self.get_serializer(instance, data=request.data, partial=True)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+
+ return Response(serializer.data)
diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
new file mode 100644
index 00000000..df51917d
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
@@ -0,0 +1,98 @@
+from django.shortcuts import get_object_or_404
+from rest_framework.exceptions import ParseError
+from rest_framework.mixins import DestroyModelMixin
+from rest_framework.response import Response
+from rest_framework.status import HTTP_201_CREATED
+from rest_framework.viewsets import ViewSet
+
+from pydis_site.apps.api.models.bot.off_topic_channel_name import OffTopicChannelName
+from pydis_site.apps.api.serializers import OffTopicChannelNameSerializer
+
+
+class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
+ """
+ View of off-topic channel names used by the bot
+ to rotate our off-topic names on a daily basis.
+
+ ## Routes
+ ### GET /bot/off-topic-channel-names
+ Return all known off-topic channel names from the database.
+ If the `random_items` query parameter is given, for example using...
+ $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5
+ ... then the API will return `5` random items from the database.
+
+ #### Response format
+ Return a list of off-topic-channel names:
+ >>> [
+ ... "lemons-lemonade-stand",
+ ... "bbq-with-bisk"
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+ - 400: returned when `random_items` is not a positive integer
+
+ ### POST /bot/off-topic-channel-names
+ Create a new off-topic-channel name in the database.
+ The name must be given as a query parameter, for example:
+ $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?name=lemons-lemonade-shop
+
+ #### Status codes
+ - 201: returned on success
+ - 400: if the request body has invalid fields, see the response for details
+
+ ### DELETE /bot/off-topic-channel-names/<name:str>
+ Delete the off-topic-channel name with the given `name`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: returned when the given `name` was not found
+
+ ## Authentication
+ Requires a API token.
+ """
+
+ lookup_field = 'name'
+ serializer_class = OffTopicChannelNameSerializer
+
+ def get_object(self):
+ queryset = self.get_queryset()
+ name = self.kwargs[self.lookup_field]
+ return get_object_or_404(queryset, name=name)
+
+ def get_queryset(self):
+ return OffTopicChannelName.objects.all()
+
+ def create(self, request):
+ if 'name' in request.query_params:
+ create_data = {'name': request.query_params['name']}
+ serializer = OffTopicChannelNameSerializer(data=create_data)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return Response(create_data, status=HTTP_201_CREATED)
+
+ else:
+ raise ParseError(detail={
+ 'name': ["This query parameter is required."]
+ })
+
+ def list(self, request): # noqa
+ if 'random_items' in request.query_params:
+ param = request.query_params['random_items']
+ try:
+ random_count = int(param)
+ except ValueError:
+ raise ParseError(detail={'random_items': ["Must be a valid integer."]})
+
+ if random_count <= 0:
+ raise ParseError(detail={
+ 'random_items': ["Must be a positive integer."]
+ })
+
+ queryset = self.get_queryset().order_by('?')[:random_count]
+ serialized = self.serializer_class(queryset, many=True)
+ return Response(serialized.data)
+
+ queryset = self.get_queryset()
+ serialized = self.serializer_class(queryset, many=True)
+ return Response(serialized.data)
diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py
new file mode 100644
index 00000000..d4ac8c76
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/reminder.py
@@ -0,0 +1,66 @@
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.filters import SearchFilter
+from rest_framework.mixins import (
+ CreateModelMixin,
+ DestroyModelMixin,
+ ListModelMixin
+)
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot.reminder import Reminder
+from pydis_site.apps.api.serializers import ReminderSerializer
+
+
+class ReminderViewSet(CreateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet):
+ """
+ View providing CRUD access to reminders.
+
+ ## Routes
+ ### GET /bot/reminders
+ Returns all reminders in the database.
+
+ #### Response format
+ >>> [
+ ... {
+ ... 'active': True,
+ ... 'author': 1020103901030,
+ ... 'content': "Make dinner",
+ ... 'expiration': '5018-11-20T15:52:00Z',
+ ... 'id': 11
+ ... },
+ ... ...
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+
+ ### POST /bot/reminders
+ Create a new reminder.
+
+ #### Request body
+ >>> {
+ ... 'author': int,
+ ... 'content': str,
+ ... 'expiration': str # ISO-formatted datetime
+ ... }
+
+ #### Status codes
+ - 201: returned on success
+ - 400: if the body format is invalid
+ - 404: if no user with the given ID could be found
+
+ ### DELETE /bot/reminders/<id:int>
+ Delete the reminder with the given `id`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: if a reminder with the given `id` does not exist
+
+ ## Authentication
+ Requires an API token.
+ """
+
+ serializer_class = ReminderSerializer
+ queryset = Reminder.objects.prefetch_related('author')
+ filter_backends = (DjangoFilterBackend, SearchFilter)
+ filter_fields = ('active', 'author__id')
diff --git a/pydis_site/apps/api/viewsets/bot/role.py b/pydis_site/apps/api/viewsets/bot/role.py
new file mode 100644
index 00000000..0131b374
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/role.py
@@ -0,0 +1,95 @@
+from rest_framework.viewsets import ModelViewSet
+
+from pydis_site.apps.api.models.bot.role import Role
+from pydis_site.apps.api.serializers import RoleSerializer
+
+
+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
diff --git a/pydis_site/apps/api/viewsets/bot/snake_fact.py b/pydis_site/apps/api/viewsets/bot/snake_fact.py
new file mode 100644
index 00000000..0b2e8ede
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/snake_fact.py
@@ -0,0 +1,30 @@
+from rest_framework.mixins import ListModelMixin
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot.snake_fact import SnakeFact
+from pydis_site.apps.api.serializers import SnakeFactSerializer
+
+
+class SnakeFactViewSet(ListModelMixin, GenericViewSet):
+ """
+ View providing snake facts created by the Pydis community in the first code jam.
+
+ ## Routes
+ ### GET /bot/snake-facts
+ Returns snake facts from the database.
+
+ #### Response format
+ >>> [
+ ... {'fact': 'Snakes are dangerous'},
+ ... {'fact': 'Except for Python, we all love it'}
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+
+ ## Authentication
+ Requires an API token.
+ """
+
+ serializer_class = SnakeFactSerializer
+ queryset = SnakeFact.objects.all()
diff --git a/pydis_site/apps/api/viewsets/bot/snake_idiom.py b/pydis_site/apps/api/viewsets/bot/snake_idiom.py
new file mode 100644
index 00000000..9f274d2f
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/snake_idiom.py
@@ -0,0 +1,30 @@
+from rest_framework.mixins import ListModelMixin
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot.snake_idiom import SnakeIdiom
+from pydis_site.apps.api.serializers import SnakeIdiomSerializer
+
+
+class SnakeIdiomViewSet(ListModelMixin, GenericViewSet):
+ """
+ View providing snake idioms for the snake cog.
+
+ ## Routes
+ ### GET /bot/snake-idioms
+ Returns snake idioms from the database.
+
+ #### Response format
+ >>> [
+ ... {'idiom': 'Sneky snek'},
+ ... {'idiom': 'Snooky Snake'}
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+
+ ## Authentication
+ Requires an API token
+ """
+
+ serializer_class = SnakeIdiomSerializer
+ queryset = SnakeIdiom.objects.all()
diff --git a/pydis_site/apps/api/viewsets/bot/snake_name.py b/pydis_site/apps/api/viewsets/bot/snake_name.py
new file mode 100644
index 00000000..991706f5
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/snake_name.py
@@ -0,0 +1,61 @@
+from rest_framework.response import Response
+from rest_framework.viewsets import ViewSet
+
+from pydis_site.apps.api.models.bot.snake_name import SnakeName
+from pydis_site.apps.api.serializers import SnakeNameSerializer
+
+
+class SnakeNameViewSet(ViewSet):
+ """
+ View providing snake names for the bot's snake cog from our first code jam's winners.
+
+ ## Routes
+ ### GET /bot/snake-names
+ By default, return a single random snake name along with its name and scientific name.
+ If the `get_all` query parameter is given, for example using...
+ $ curl api.pythondiscord.local:8000/bot/snake-names?get_all=yes
+ ... then the API will return all snake names and scientific names in the database.
+
+ #### Response format
+ Without `get_all` query parameter:
+ >>> {
+ ... 'name': "Python",
+ ... 'scientific': "Langus greatus"
+ ... }
+
+ If the database is empty for whatever reason, this will return an empty dictionary.
+
+ With `get_all` query parameter:
+ >>> [
+ ... {'name': "Python 3", 'scientific': "Langus greatus"},
+ ... {'name': "Python 2", 'scientific': "Langus decentus"}
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+
+ ## Authentication
+ Requires a API token.
+ """
+
+ serializer_class = SnakeNameSerializer
+
+ def get_queryset(self):
+ return SnakeName.objects.all()
+
+ def list(self, request): # noqa
+ if request.query_params.get('get_all'):
+ queryset = self.get_queryset()
+ serialized = self.serializer_class(queryset, many=True)
+ return Response(serialized.data)
+
+ single_snake = SnakeName.objects.order_by('?').first()
+ if single_snake is not None:
+ body = {
+ 'name': single_snake.name,
+ 'scientific': single_snake.scientific
+ }
+
+ return Response(body)
+
+ return Response({})
diff --git a/pydis_site/apps/api/viewsets/bot/special_snake.py b/pydis_site/apps/api/viewsets/bot/special_snake.py
new file mode 100644
index 00000000..446c79a1
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/special_snake.py
@@ -0,0 +1,33 @@
+from rest_framework.mixins import ListModelMixin
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot import SpecialSnake
+from pydis_site.apps.api.serializers import SpecialSnakeSerializer
+
+
+class SpecialSnakeViewSet(ListModelMixin, GenericViewSet):
+ """
+ View providing special snake names for our bot's snake cog.
+
+ ## Routes
+ ### GET /bot/special-snakes
+ Returns a list of special snake names.
+
+ #### Response Format
+ >>> [
+ ... {
+ ... 'name': 'Snakky sneakatus',
+ ... 'info': 'Scary snek',
+ ... 'image': 'https://discordapp.com/assets/53ef346458017da2062aca5c7955946b.svg'
+ ... }
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+
+ ## Authentication
+ Requires an API token.
+ """
+
+ serializer_class = SpecialSnakeSerializer
+ queryset = SpecialSnake.objects.all()
diff --git a/pydis_site/apps/api/viewsets/bot/tag.py b/pydis_site/apps/api/viewsets/bot/tag.py
new file mode 100644
index 00000000..7e9ba117
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/tag.py
@@ -0,0 +1,105 @@
+from rest_framework.viewsets import ModelViewSet
+
+from pydis_site.apps.api.models.bot.tag import Tag
+from pydis_site.apps.api.serializers import TagSerializer
+
+
+class TagViewSet(ModelViewSet):
+ """
+ View providing CRUD operations on tags shown by our bot.
+
+ ## Routes
+ ### GET /bot/tags
+ Returns all tags in the database.
+
+ #### Response format
+ >>> [
+ ... {
+ ... 'title': "resources",
+ ... 'embed': {
+ ... 'content': "Did you really think I'd put something useful here?"
+ ... }
+ ... }
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+
+ ### GET /bot/tags/<title:str>
+ Gets a single tag by its title.
+
+ #### Response format
+ >>> {
+ ... 'title': "My awesome tag",
+ ... 'embed': {
+ ... 'content': "totally not filler words"
+ ... }
+ ... }
+
+ #### Status codes
+ - 200: returned on success
+ - 404: if a tag with the given `title` could not be found
+
+ ### POST /bot/tags
+ Adds a single tag to the database.
+
+ #### Request body
+ >>> {
+ ... 'title': str,
+ ... 'embed': dict
+ ... }
+
+ The embed structure is the same as the embed structure that the Discord API
+ expects. You can view the documentation for it here:
+ https://discordapp.com/developers/docs/resources/channel#embed-object
+
+ #### Status codes
+ - 201: returned on success
+ - 400: if one of the given fields is invalid
+
+ ### PUT /bot/tags/<title:str>
+ Update the tag with the given `title`.
+
+ #### Request body
+ >>> {
+ ... 'title': str,
+ ... 'embed': dict
+ ... }
+
+ The embed structure is the same as the embed structure that the Discord API
+ expects. You can view the documentation for it here:
+ https://discordapp.com/developers/docs/resources/channel#embed-object
+
+ #### Status codes
+ - 200: returned on success
+ - 400: if the request body was invalid, see response body for details
+ - 404: if the tag with the given `title` could not be found
+
+ ### PATCH /bot/tags/<title:str>
+ Update the tag with the given `title`.
+
+ #### Request body
+ >>> {
+ ... 'title': str,
+ ... 'embed': dict
+ ... }
+
+ The embed structure is the same as the embed structure that the Discord API
+ expects. You can view the documentation for it here:
+ https://discordapp.com/developers/docs/resources/channel#embed-object
+
+ #### Status codes
+ - 200: returned on success
+ - 400: if the request body was invalid, see response body for details
+ - 404: if the tag with the given `title` could not be found
+
+ ### DELETE /bot/tags/<title:str>
+ Deletes the tag with the given `title`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: if a tag with the given `title` does not exist
+ """
+
+ serializer_class = TagSerializer
+ queryset = Tag.objects.all()
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
new file mode 100644
index 00000000..a407787e
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -0,0 +1,126 @@
+from rest_framework.viewsets import ModelViewSet
+from rest_framework_bulk import BulkCreateModelMixin
+
+from pydis_site.apps.api.models.bot.user import User
+from pydis_site.apps.api.serializers import UserSerializer
+
+
+class UserViewSet(BulkCreateModelMixin, ModelViewSet):
+ """
+ View providing CRUD operations on Discord users through the bot.
+
+ ## Routes
+ ### GET /bot/users
+ Returns all users currently known.
+
+ #### Response format
+ >>> [
+ ... {
+ ... 'id': 409107086526644234,
+ ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb",
+ ... 'name': "Python",
+ ... 'discriminator': 4329,
+ ... 'roles': [
+ ... 352427296948486144,
+ ... 270988689419665409,
+ ... 277546923144249364,
+ ... 458226699344019457
+ ... ],
+ ... 'in_guild': True
+ ... }
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+
+ ### GET /bot/users/<snowflake:int>
+ Gets a single user by ID.
+
+ #### Response format
+ >>> {
+ ... 'id': 409107086526644234,
+ ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb",
+ ... 'name': "Python",
+ ... 'discriminator': 4329,
+ ... 'roles': [
+ ... 352427296948486144,
+ ... 270988689419665409,
+ ... 277546923144249364,
+ ... 458226699344019457
+ ... ],
+ ... 'in_guild': True
+ ... }
+
+ #### Status codes
+ - 200: returned on success
+ - 404: if a user with the given `snowflake` could not be found
+
+ ### POST /bot/users
+ Adds a single or multiple new users.
+ The roles attached to the user(s) must be roles known by the site.
+
+ #### Request body
+ >>> {
+ ... 'id': int,
+ ... 'avatar': str,
+ ... 'name': str,
+ ... 'discriminator': int,
+ ... 'roles': List[int],
+ ... 'in_guild': bool
+ ... }
+
+ Alternatively, request users can be POSTed as a list of above objects,
+ in which case multiple users will be created at once.
+
+ #### Status codes
+ - 201: returned on success
+ - 400: if one of the given roles does not exist, or one of the given fields is invalid
+
+ ### PUT /bot/users/<snowflake:int>
+ Update the user with the given `snowflake`.
+ All fields in the request body are required.
+
+ #### Request body
+ >>> {
+ ... 'id': int,
+ ... 'avatar': str,
+ ... 'name': str,
+ ... 'discriminator': int,
+ ... 'roles': List[int],
+ ... 'in_guild': bool
+ ... }
+
+ #### Status codes
+ - 200: returned on success
+ - 400: if the request body was invalid, see response body for details
+ - 404: if the user with the given `snowflake` could not be found
+
+ ### PATCH /bot/users/<snowflake:int>
+ Update the user with the given `snowflake`.
+ All fields in the request body are optional.
+
+ #### Request body
+ >>> {
+ ... 'id': int,
+ ... 'avatar': str,
+ ... 'name': str,
+ ... 'discriminator': int,
+ ... 'roles': List[int],
+ ... 'in_guild': bool
+ ... }
+
+ #### Status codes
+ - 200: returned on success
+ - 400: if the request body was invalid, see response body for details
+ - 404: if the user with the given `snowflake` could not be found
+
+ ### DELETE /bot/users/<snowflake:int>
+ Deletes the user with the given `snowflake`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: if a user with the given `snowflake` does not exist
+ """
+
+ serializer_class = UserSerializer
+ queryset = User.objects.prefetch_related('roles')
diff --git a/pydis_site/apps/api/viewsets/log_entry.py b/pydis_site/apps/api/viewsets/log_entry.py
new file mode 100644
index 00000000..4aa7dffa
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/log_entry.py
@@ -0,0 +1,37 @@
+from rest_framework.mixins import CreateModelMixin
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.log_entry import LogEntry
+from pydis_site.apps.api.serializers import LogEntrySerializer
+
+
+class LogEntryViewSet(CreateModelMixin, GenericViewSet):
+ """
+ View providing support for creating log entries in the site database
+ for viewing via the log browser.
+
+ ## Routes
+ ### POST /logs
+ Create a new log entry.
+
+ #### Request body
+ >>> {
+ ... 'application': str, # 'bot' | 'seasonalbot' | 'site'
+ ... 'logger_name': str, # such as 'bot.cogs.moderation'
+ ... 'timestamp': Optional[str], # from `datetime.utcnow().isoformat()`
+ ... 'level': str, # 'debug' | 'info' | 'warning' | 'error' | 'critical'
+ ... 'module': str, # such as 'pydis_site.apps.api.serializers'
+ ... 'line': int, # > 0
+ ... 'message': str, # textual formatted content of the logline
+ ... }
+
+ #### Status codes
+ - 201: returned on success
+ - 400: if the request body has invalid fields, see the response for details
+
+ ## Authentication
+ Requires a API token.
+ """
+
+ queryset = LogEntry.objects.all()
+ serializer_class = LogEntrySerializer