diff options
Diffstat (limited to 'pydis_site/apps/api/viewsets.py')
-rw-r--r-- | pydis_site/apps/api/viewsets.py | 923 |
1 files changed, 0 insertions, 923 deletions
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) |