diff --git a/apps/home/views.py b/apps/home/views.py index 54db893..1222e96 100644 --- a/apps/home/views.py +++ b/apps/home/views.py @@ -6,9 +6,8 @@ import httpx from django.conf import settings from asgiref.sync import sync_to_async from django.shortcuts import redirect -from django.http import JsonResponse, HttpResponseNotFound +from django.http import JsonResponse, HttpResponseNotFound, HttpResponseNotAllowed from django.views.generic import TemplateView, View -from django.core.exceptions import PermissionDenied from apps.home.models import Server, DiscordChannel from apps.authentication.models import DiscordUser, ServerMember @@ -34,52 +33,92 @@ def is_user_authenticated(user: DiscordUser) -> bool: class GuildsView(View): + """ + Fetches the related guilds to the currently authenticated user from Discord. + Will return a filtered list of the results, excluding servers where the + user isn't an administrator or owner. + + Valid servers will also be stored in the database for future reference, + along-side the user-server relationship as a member. + """ async def get(self, request, *args, **kwargs): if not await is_user_authenticated(request.user): return redirect("/oauth2/login") access_token = await get_user_access_token(request.user) + guilds_data, status = await self._get_guilds_data(access_token) + + # Send back the error data if status is bad + if status != 200: + return JsonResponse(guilds_data, status=status, safe=False) + + cleaned_guilds_data = await self._clean_guilds_data(request.user, guilds_data) + return JsonResponse(cleaned_guilds_data, safe=False) + + async def _get_guilds_data(self, access_token: str) -> tuple[list[dict], int]: + """ + Returns the raw guild data and a response status code + from the Discord API. + """ async with httpx.AsyncClient() as client: response = await client.get( url=f"{settings.DISCORD_API_URL}/users/@me/guilds", headers={"Authorization": f"Bearer {access_token}"} ) - status = response.status_code - guild_data = response.json() + return response.json(), response.status_code - if status != 200: - return JsonResponse(guild_data, status=status, safe=False) + async def _clean_guilds_data(self, user: DiscordUser, guilds_data: list[dict]) -> list[dict]: + """ + Returns a filtered copy of the given `guilds_data`, without the guilds + where the given `user` is not an administrator or owner of. - # guilds where the user either administrates or owns - cleaned_guild_data = [] + Also, for each guild, creates/updates an object to represent it, and + another to represent the user/guild relationship as a member. + """ - for item in guild_data: - cleaned_data = await self.setup_server(request.user, item) - if cleaned_data: - cleaned_guild_data.append(cleaned_data) + cleaned_guilds_data = [] - return JsonResponse(cleaned_guild_data, safe=False) + for item in guilds_data: + setup_success = await self._setup_server(user, item) + if setup_success: + cleaned_guilds_data.append(item) - async def setup_server(self, user: DiscordUser, data: dict): - is_owner = data["owner"] - permissions = data["permissions"] + return cleaned_guilds_data + + async def _setup_server(self, user: DiscordUser, item: dict) -> bool: + """ + Create or update a server and user's membership to said server. + Returns `True` if successful, returns `False` if the user isn't an + administrator or owner of the given server. + + If `False`, will also attempt to delete any existing member object + linked to the given server. + """ + + # Collect some commonly used server data + server_id = item["id"] + is_owner = item["owner"] + permissions = item["permissions"] admin_perm = 1 << 3 - # Ignore servers where the user isn't an administrator or owner + # Skip servers where the user isn't an administrator or owner. + # If an older member object exists, delete that too. if not ((int(permissions) & admin_perm) == admin_perm or is_owner): - await self.delete_member(user, data["id"]) - return + await self._try_delete_member(user, server_id) + return False + # Create or update an existing server matching the given ID server = await Server.objects.aupdate_or_create( - id=data["id"], + id=server_id, defaults={ - "name": data["name"], - "icon_hash": data["icon"] + "name": item["name"], + "icon_hash": item["icon"] } ) - + + # Create or update a member object linking the user to the server await ServerMember.objects.aupdate_or_create( user=user, server=server[0], @@ -89,10 +128,15 @@ class GuildsView(View): } ) - return data + return True @staticmethod - async def delete_member(user: DiscordUser, server_id: int): + async def _try_delete_member(user: DiscordUser, server_id: int): + """ + Attempt to delete any existing server member linked to the given + server id. + """ + try: member = await ServerMember.objects.aget(user=user, server_id=server_id) await member.adelete() @@ -101,21 +145,20 @@ class GuildsView(View): class ChannelsView(View): - async def get(self, request, *args, **kwargs): if not await is_user_authenticated(request.user): return redirect("/oauth2/login") guild_id = request.GET.get("guild") - try: - server = await Server.objects.aget(pk=guild_id) - except Server.DoesNotExist: - return HttpResponseNotFound("Server not found.") + try: server = await Server.objects.aget(pk=guild_id) + except Server.DoesNotExist: return HttpResponseNotFound("Server not found.") if not ServerMember.objects.filter(server=server, user=request.user).aexists(): - raise PermissionDenied("You aren't a member of this server.") + return HttpResponseNotAllowed("You aren't a member of this server.") channels_data, status = await self._get_channel_data(guild_id) + + # Send back the error data if status is bad if status != 200: return JsonResponse(channels_data, status=status, safe=False) @@ -159,6 +202,7 @@ class ChannelsView(View): `data` dictionary, returns the data or `NoneType` if `data['type'] != 0`. """ + # Type 0 = TextChannel, the only one we want if data.get("type") != 0: return