From ecfd4a37f3f284ba20ee247fd3d64f706509ea5d Mon Sep 17 00:00:00 2001 From: Corban-Lee Jones Date: Fri, 20 Sep 2024 00:10:27 +0100 Subject: [PATCH] server setup via api --- apps/api/errors.py | 3 + apps/api/serializers.py | 32 +++++++- apps/api/urls.py | 52 +++++++------ apps/api/views.py | 77 +++++++++++++++++-- .../migrations/0004_servermember.py | 26 +++++++ apps/authentication/models.py | 22 ++++++ apps/home/models.py | 14 ---- 7 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 apps/api/errors.py create mode 100644 apps/authentication/migrations/0004_servermember.py diff --git a/apps/api/errors.py b/apps/api/errors.py new file mode 100644 index 0000000..f12a23c --- /dev/null +++ b/apps/api/errors.py @@ -0,0 +1,3 @@ + +class NotAMemberError(Exception): + pass diff --git a/apps/api/serializers.py b/apps/api/serializers.py index 1532734..5c794da 100644 --- a/apps/api/serializers.py +++ b/apps/api/serializers.py @@ -264,8 +264,11 @@ class TrackedContentSerializer_POST(DynamicModelSerializer): # rewrite +class DiscordServerIdSerializer(serializers.Serializer): + server_id = serializers.IntegerField() -class r_ServerSerialiszer(DynamicModelSerializer): + +class r_ServerSerializer(DynamicModelSerializer): class Meta: model = r_Server fields = ("id", "name", "icon_hash", "active") @@ -309,6 +312,11 @@ class r_MessageStyleSerializer(DynamicModelSerializer): class r_SubscriptionSerializer(DynamicModelSerializer): + filters = serializers.PrimaryKeyRelatedField( + queryset=r_ContentFilter.objects.all(), + many=True + ) + class Meta: model = r_Subscription fields = ( @@ -324,6 +332,28 @@ class r_SubscriptionSerializer(DynamicModelSerializer): "message_style" ) + def validate(self, data): + server = data.get("server") or self.context.get("server") + if not server: + return data + + # Prevent using filters from a different server + selected_filters = data.get("filters", []) + valid_filter_ids = r_ContentFilter.objects.filter(server=server).values_list("id", flat=True) + if any(fltr.id not in valid_filter_ids for fltr in selected_filters): + raise serializers.ValidationError( + {"filters": "All filters must belong to the specified server."} + ) + + # Prevent using message styles from a different server + message_style = data.get("message_style") + if message_style and message_style.server != server: + raise serializers.ValidationError( + {"message_style": "Message style must belong to the specified server."} + ) + + return data + class r_ContentSerializer(DynamicModelSerializer): class Meta: diff --git a/apps/api/urls.py b/apps/api/urls.py index b6cd134..a60552e 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -23,6 +23,7 @@ from .views import ( UniqueContentRule_DetailView, #rewrite + CreateDiscordServerView, r_Server_ListView, r_Server_DetailView, r_ContentFilter_ListView, @@ -86,40 +87,43 @@ urlpatterns = [ path("/", UniqueContentRule_DetailView.as_view(), name="unique-content-rule-detail") ])), + #region rewrite + path("discord-servers/", CreateDiscordServerView.as_view()), + path("r_servers/", include([ path("", r_Server_ListView.as_view()), path("/", r_Server_DetailView.as_view()) ])), - # path("r_content-filters/", include([ - # path(""), - # path("/") - # ])), + path("r_content-filters/", include([ + path("", r_ContentFilter_ListView.as_view()), + path("/", r_ContentFilter_DetailView.as_view()) + ])), - # path("r_message-mutators/", include([ - # path(""), - # path("/") - # ])), + path("r_message-mutators/", include([ + path("", r_MessageMutator_ListView.as_view()), + path("/", r_MessageMutator_DetailView.as_view()) + ])), - # path("r_message-styles/", include([ - # path(""), - # path("/") - # ])), + path("r_message-styles/", include([ + path("", r_MessageStyle_ListView.as_view()), + path("/", r_MessageStyle_DetailView.as_view()) + ])), - # path("r_subscriptions/", include([ - # path(""), - # path("/") - # ])), + path("r_subscriptions/", include([ + path("", r_Subscription_ListView.as_view()), + path("/", r_Subscription_DetailView.as_view()) + ])), - # path("r_content/", include([ - # path(""), - # path("/") - # ])), + path("r_content/", include([ + path("", r_Content_ListView.as_view()), + path("/", r_Content_DetailView.as_view()) + ])), - # path("r_unique-content-rules/", include([ - # path(""), - # path("/") - # ])) + path("r_unique-content-rules/", include([ + path("", r_UniqueContentRule_ListView.as_view()), + path("/", r_UniqueContentRule_DetailView.as_view()) + ])) ] diff --git a/apps/api/views.py b/apps/api/views.py index 24ee1b0..0564d61 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -1,7 +1,9 @@ # -*- encoding: utf-8 -*- import logging +import requests +from django.conf import settings from django.db.models import Subquery from django.db.utils import IntegrityError from django_filters import rest_framework as rest_filters @@ -30,7 +32,7 @@ from apps.home.models import ( r_Content, r_UniqueContentRule ) -from apps.authentication.models import DiscordUser +from apps.authentication.models import DiscordUser, ServerMember from .metadata import ExpandedMetadata from .serializers import ( SubChannelSerializer, @@ -45,7 +47,8 @@ from .serializers import ( UniqueContentRuleSerializer, #rewrite - r_ServerSerialiszer, + DiscordServerIdSerializer, + r_ServerSerializer, r_ContentFilterSerializer, r_MessageMutatorSerializer, r_MessageStyleSerializer, @@ -53,6 +56,7 @@ from .serializers import ( r_ContentSerializer, r_UniqueContentRuleSerializer ) +from .errors import NotAMemberError log = logging.getLogger(__name__) @@ -708,18 +712,81 @@ class DeletableDetailView(generics.RetrieveDestroyAPIView): parser_classes = [MultiPartParser, FormParser] +class CreateDiscordServerView(generics.CreateAPIView): + serializer_class = DiscordServerIdSerializer + + def post(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data) + if not serializer.is_valid(): + return Response() + + server_id = serializer.validated_data["server_id"] + response = requests.get( + url=f"{settings.DISCORD_API_URL}/guilds/{server_id}", + headers={"Authorization": f"Bot {settings.BOT_TOKEN}"} + ) + raw = response.json() + if not response.status_code == 200: + return Response( + status=response.status_code, + data=raw + ) + + server = r_Server.objects.filter(id=server_id) + + if server.exists(): + return self.create_member_for_server(server.first(), request.user) + else: + return self.create_server(raw, request.user) + + + def create_member_for_server(self, server: r_Server, user: DiscordUser) -> Response: + response = requests.get( + url=f"{settings.DISCORD_API_URL}/users/@me/guilds/{server.id}/member", # TODO: continue here + headers={"Authorization": f"Bearer {user.access_token}"} # the scope of the token doesnt cover membership, so + ) # this needs to be updated against the bot from the + raw = response.json() # discord developers dashboard. + if response.status_code != 200: + return Response( + status=response.status_code, + data=raw + ) + + # TODO: might need to a case where this member already exists + ServerMember.objects.get_or_create( + server=server, + user=user, + nick=raw.get("nick"), + permissions=raw["permissions"] + ) + + return Response( + status=200, + data={"message": "success"} + ) + + def create_server(self, raw: dict, user: DiscordUser) -> Response: + server = r_Server.objects.create( + id=raw["id"], + name=raw["name"], + icon_hash=raw["icon"] + ) + + return self.create_member_for_server(server, user) + + class r_Server_ListView(ListCreateView): # maybe change to ListView only later, and create through secure backend means? filterset_fields = [] search_fields = [] ordering_fields = [] - serializer_class = r_ServerSerialiszer + serializer_class = r_ServerSerializer def get_queryset(self): return r_Server.objects.all() class r_Server_DetailView(ChangableDetailView): # maybe change to ListView only later, and create through secure backend means? - serializer_class = r_ServerSerialiszer + serializer_class = r_ServerSerializer def get_queryset(self): return r_Server.objects.all() @@ -742,7 +809,7 @@ class r_ContentFilter_DetailView(ChangableDetailView): return r_ContentFilter.objects.all() -class r_MessageMutator_ListView(): # instances of this one are pre-defined ONLY +class r_MessageMutator_ListView(ListView): # instances of this one are pre-defined ONLY filterset_fields = [] search_fields = [] ordering_fields = [] diff --git a/apps/authentication/migrations/0004_servermember.py b/apps/authentication/migrations/0004_servermember.py new file mode 100644 index 0000000..28563e5 --- /dev/null +++ b/apps/authentication/migrations/0004_servermember.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.4 on 2024-09-19 20:20 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0003_discorduser_refresh_token'), + ('home', '0026_temp_testonly_migration'), + ] + + operations = [ + migrations.CreateModel( + name='ServerMember', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('nick', models.CharField(max_length=32)), + ('permissions', models.CharField(max_length=32)), + ('server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.r_server')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index c0a1e4e..0b78fc1 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -159,3 +159,25 @@ class DiscordUser(PermissionsMixin): self.refresh_token=raw["refresh_token"] self.save(force_update=True) + + +class ServerMember(models.Model): + """ + A link table, connecting users to servers, and storing their server-specific data. + """ + + id = models.AutoField(primary_key=True) + user = models.ForeignKey(to=DiscordUser, on_delete=models.CASCADE) + server = models.ForeignKey(to="home.r_Server", on_delete=models.CASCADE) + nick = models.CharField(max_length=32, null=True, blank=True) + permissions = models.CharField(max_length=32) + + def __str__(self): + return f"{self.server.name} ยท {self.user}({self.nick})" + + def has_permission(self, flag: int) -> bool: + """Check that the member has a givern permission. + https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags + """ + permissions_int = int(self.permissions) + return (permissions_int & flag) == flag diff --git a/apps/home/models.py b/apps/home/models.py index 702f531..4385438 100644 --- a/apps/home/models.py +++ b/apps/home/models.py @@ -517,20 +517,6 @@ class r_Subscription(models.Model): filters = models.ManyToManyField(to=r_ContentFilter, blank=True) message_style = models.ForeignKey(to=r_MessageStyle, on_delete=models.SET_NULL, null=True, blank=True) - def clean(self): - if self.message_style and self.message_style.server != self.server: - raise ValidationError("Cannot use any Message Style of another server.") - - if any(fltr.server != self.server for fltr in self.filters.all()): - raise ValidationError("Cannot use any Content Filter of another server.") - - def save(self, *args, **kwargs): - if not self.pk: - super().save(*args, **kwargs) - - self.clean() - super().save(*args, **kwargs) - def __str__(self): return self.name