diff options
Diffstat (limited to '')
18 files changed, 1018 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) 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 | 
