diff --git a/apps/api/serializers.py b/apps/api/serializers.py index ab8d4b7..1532734 100644 --- a/apps/api/serializers.py +++ b/apps/api/serializers.py @@ -12,7 +12,16 @@ from apps.home.models import ( TrackedContent, ArticleMutator, GuildSettings, - UniqueContentRule + UniqueContentRule, + + #rewrite + r_Server, + r_ContentFilter, + r_MessageMutator, + r_MessageStyle, + r_Subscription, + r_Content, + r_UniqueContentRule ) log = logging.getLogger(__name__) @@ -248,3 +257,89 @@ class TrackedContentSerializer_POST(DynamicModelSerializer): class Meta: model = TrackedContent fields = ("id", "guid", "title", "url", "subscription", "channel_id", "message_id", "blocked", "creation_datetime") + + + + + +# rewrite + + +class r_ServerSerialiszer(DynamicModelSerializer): + class Meta: + model = r_Server + fields = ("id", "name", "icon_hash", "active") + + +class r_ContentFilterSerializer(DynamicModelSerializer): + class Meta: + model = r_ContentFilter + fields = ( + "id", + "server", + "name", + "match", + "matching_algorithm", + "is_insensitive", + "is_whitelist" + ) + + +class r_MessageMutatorSerializer(DynamicModelSerializer): + class Meta: + model = r_MessageMutator + fields = ("id", "name", "value") + + +class r_MessageStyleSerializer(DynamicModelSerializer): + class Meta: + model = r_MessageStyle + fields = ( + "id", + "server", + "is_embed", + "is_hyperlinked", + "show_author", + "show_timestamp", + "show_images", + "fetch_images", + "title_mutator", + "description_mutator" + ) + + +class r_SubscriptionSerializer(DynamicModelSerializer): + class Meta: + model = r_Subscription + fields = ( + "id", + "server", + "name", + "url", + "created_at", + "updated_at", + "extra_notes", + "active", + "filters", + "message_style" + ) + + +class r_ContentSerializer(DynamicModelSerializer): + class Meta: + model = r_Content + fields = ( + "id", + "subscription", + "item_id", + "item_guid", + "item_url", + "item_title", + "item_content_hash" + ) + + +class r_UniqueContentRuleSerializer(DynamicModelSerializer): + class Meta: + model = r_UniqueContentRule + fields = ("id", "name", "value") diff --git a/apps/api/urls.py b/apps/api/urls.py index 9d8a758..b6cd134 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -20,7 +20,23 @@ from .views import ( GuildSettings_ListView, GuildSettings_DetailView, UniqueContentRule_ListView, - UniqueContentRule_DetailView + UniqueContentRule_DetailView, + + #rewrite + r_Server_ListView, + r_Server_DetailView, + r_ContentFilter_ListView, + r_ContentFilter_DetailView, + r_MessageMutator_ListView, + r_MessageMutator_DetailView, + r_MessageStyle_ListView, + r_MessageStyle_DetailView, + r_Subscription_ListView, + r_Subscription_DetailView, + r_Content_ListView, + r_Content_DetailView, + r_UniqueContentRule_ListView, + r_UniqueContentRule_DetailView ) urlpatterns = [ @@ -69,4 +85,41 @@ urlpatterns = [ path("", UniqueContentRule_ListView.as_view(), name="unique-content-rule"), path("/", UniqueContentRule_DetailView.as_view(), name="unique-content-rule-detail") ])), + + #region rewrite + + 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_message-mutators/", include([ + # path(""), + # path("/") + # ])), + + # path("r_message-styles/", include([ + # path(""), + # path("/") + # ])), + + # path("r_subscriptions/", include([ + # path(""), + # path("/") + # ])), + + # path("r_content/", include([ + # path(""), + # path("/") + # ])), + + # path("r_unique-content-rules/", include([ + # path(""), + # path("/") + # ])) ] diff --git a/apps/api/views.py b/apps/api/views.py index ba4eb09..24ee1b0 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -11,7 +11,25 @@ from rest_framework.pagination import PageNumberPagination from rest_framework.authentication import SessionAuthentication, TokenAuthentication from rest_framework.parsers import MultiPartParser, FormParser -from apps.home.models import SubChannel, Filter, Subscription, SavedGuilds, TrackedContent, ArticleMutator, GuildSettings, UniqueContentRule +from apps.home.models import ( + SubChannel, + Filter, + Subscription, + SavedGuilds, + TrackedContent, + ArticleMutator, + GuildSettings, + UniqueContentRule, + + #rewrite + r_Server, + r_ContentFilter, + r_MessageMutator, + r_MessageStyle, + r_Subscription, + r_Content, + r_UniqueContentRule +) from apps.authentication.models import DiscordUser from .metadata import ExpandedMetadata from .serializers import ( @@ -24,7 +42,16 @@ from .serializers import ( TrackedContentSerializer_POST, ArticleMutatorSerializer, GuildSettingsSerializer, - UniqueContentRuleSerializer + UniqueContentRuleSerializer, + + #rewrite + r_ServerSerialiszer, + r_ContentFilterSerializer, + r_MessageMutatorSerializer, + r_MessageStyleSerializer, + r_SubscriptionSerializer, + r_ContentSerializer, + r_UniqueContentRuleSerializer ) log = logging.getLogger(__name__) @@ -642,3 +669,159 @@ class ArticleMutator_DetailView(generics.RetrieveAPIView): serializer_class = ArticleMutatorSerializer queryset = ArticleMutator.objects.all().order_by("id") + + +#region rewrite + + +class ListView(generics.ListAPIView): + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + pagination_class = DefaultPagination + metadata_class = ExpandedMetadata + filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter] + + +class ListCreateView(generics.ListCreateAPIView): + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + pagination_class = DefaultPagination + metadata_class = ExpandedMetadata + filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter] + + +class DetailView(generics.RetrieveAPIView): + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + parser_classes = [MultiPartParser, FormParser] + + +class ChangableDetailView(generics.RetrieveUpdateDestroyAPIView): + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + parser_classes = [MultiPartParser, FormParser] + + +class DeletableDetailView(generics.RetrieveDestroyAPIView): + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + parser_classes = [MultiPartParser, FormParser] + + +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 + + 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 + + def get_queryset(self): + return r_Server.objects.all() + + +class r_ContentFilter_ListView(ListCreateView): + filterset_fields = [] + search_fields = [] + ordering_fields = [] + serializer_class = r_ContentFilterSerializer + + def get_queryset(self): + return r_ContentFilter.objects.all() + + +class r_ContentFilter_DetailView(ChangableDetailView): + serializer_class = r_ContentFilterSerializer + + def get_queryset(self): + return r_ContentFilter.objects.all() + + +class r_MessageMutator_ListView(): # instances of this one are pre-defined ONLY + filterset_fields = [] + search_fields = [] + ordering_fields = [] + serializer_class = r_MessageMutatorSerializer + + def get_queryset(self): + return r_MessageMutator.objects.all() + + +class r_MessageMutator_DetailView(DetailView): + serializer_class = r_MessageMutatorSerializer + + def get_queryset(self): + return r_MessageMutator.objects.all() + + +class r_MessageStyle_ListView(ListCreateView): + filterset_fields = [] + search_fields = [] + ordering_fields = [] + serializer_class = r_MessageStyleSerializer + + def get_queryset(self): + return r_MessageStyle.objects.all() + + +class r_MessageStyle_DetailView(ChangableDetailView): + serializer_class = r_MessageStyleSerializer + + def get_queryset(self): + return r_MessageStyle.objects.all() + + +class r_Subscription_ListView(ListCreateView): + filterset_fields = [] + search_fields = [] + ordering_fields = [] + serializer_class = r_SubscriptionSerializer + + def get_queryset(self): + return r_Subscription.objects.all() + + +class r_Subscription_DetailView(ChangableDetailView): + serializer_class = r_SubscriptionSerializer + + def get_queryset(self): + return r_Subscription.objects.all() + + +class r_Content_ListView(ListCreateView): + filterset_fields = [] + search_fields = [] + ordering_fields = [] + serializer_class = r_ContentSerializer + + def get_queryset(self): + return r_Content.objects.all() + + +class r_Content_DetailView(ChangableDetailView): + serializer_class = r_ContentSerializer + + def get_queryset(self): + return r_Content.objects.all() + + +class r_UniqueContentRule_ListView(ListCreateView): + filterset_fields = [] + search_fields = [] + ordering_fields = [] + serializer_class = r_UniqueContentRuleSerializer + + def get_queryset(self): + return r_UniqueContentRule.objects.all() + + +class r_UniqueContentRule_DetailView(ChangableDetailView): + serializer_class = r_UniqueContentRuleSerializer + + def get_queryset(self): + return r_UniqueContentRule.objects.all() diff --git a/apps/home/admin.py b/apps/home/admin.py index f6855fc..b75d20b 100644 --- a/apps/home/admin.py +++ b/apps/home/admin.py @@ -2,7 +2,24 @@ from django.contrib import admin -from .models import Subscription, SavedGuilds, Filter, SubChannel, TrackedContent, ArticleMutator, GuildSettings +from .models import ( + Subscription, + SavedGuilds, + Filter, + SubChannel, + TrackedContent, + ArticleMutator, + GuildSettings, + + # temp rewrite + r_Server, + r_ContentFilter, + r_MessageMutator, + r_MessageStyle, + r_Subscription, + r_Content, + r_UniqueContentRule +) @admin.register(Subscription) @@ -51,3 +68,41 @@ class GuildSettingsAdmin(admin.ModelAdmin): list_display = [ "id", "guild_id", "default_embed_colour", "active" ] + + +#region rewrite + + +@admin.register(r_Server) +class r_ServerAdmin(admin.ModelAdmin): + pass + + +@admin.register(r_ContentFilter) +class r_ContentFilterAdmin(admin.ModelAdmin): + pass + + +@admin.register(r_MessageMutator) +class r_MessageMutatorAdmin(admin.ModelAdmin): + pass + + +@admin.register(r_MessageStyle) +class r_MessageStyleAdmin(admin.ModelAdmin): + pass + + +@admin.register(r_Subscription) +class r_Subscription(admin.ModelAdmin): + pass + + +@admin.register(r_Content) +class r_ContentAdmin(admin.ModelAdmin): + pass + + +@admin.register(r_UniqueContentRule) +class r_UniqueContentRule(admin.ModelAdmin): + pass diff --git a/apps/home/migrations/0026_temp_testonly_migration.py b/apps/home/migrations/0026_temp_testonly_migration.py new file mode 100644 index 0000000..68cef51 --- /dev/null +++ b/apps/home/migrations/0026_temp_testonly_migration.py @@ -0,0 +1,104 @@ +# Generated by Django 5.0.4 on 2024-09-18 20:26 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0025_set_uniquerules'), + ] + + operations = [ + migrations.CreateModel( + name='r_MessageMutator', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('value', models.CharField(max_length=32)), + ], + ), + migrations.CreateModel( + name='r_Server', + fields=[ + ('id', models.PositiveIntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128)), + ('icon_hash', models.CharField(max_length=128)), + ('active', models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name='r_UniqueContentRule', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('value', models.CharField(max_length=32)), + ], + ), + migrations.CreateModel( + name='r_MessageStyle', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('is_embed', models.BooleanField(default=True)), + ('is_hyperlinked', models.BooleanField()), + ('show_author', models.BooleanField()), + ('show_timestamp', models.BooleanField()), + ('show_images', models.BooleanField()), + ('fetch_images', models.BooleanField()), + ('description_mutator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='desc_mutated_messagestyle', to='home.r_messagemutator')), + ('title_mutator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='title_mutated_messagestyle', to='home.r_messagemutator')), + ('server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.r_server')), + ], + ), + migrations.CreateModel( + name='r_ContentFilter', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=32)), + ('match', models.CharField(max_length=256)), + ('matching_algorithm', models.PositiveIntegerField(choices=[(0, 'None'), (1, 'Any: Item contains any of these words (space separated)'), (2, 'All: Item contains all of these words (space separated)'), (3, 'Exact: Item contains this string'), (4, 'Regular expression: Item matches this regex'), (5, 'Fuzzy: Item contains a word similar to this word')])), + ('is_insensitive', models.BooleanField()), + ('is_whitelist', models.BooleanField()), + ('server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.r_server')), + ], + ), + migrations.CreateModel( + name='BotLogicLogs', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('level', models.CharField(max_length=32)), + ('message', models.CharField(max_length=256)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ('server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.r_server')), + ], + ), + migrations.CreateModel( + name='r_Subscription', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=32)), + ('url', models.URLField()), + ('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), + ('extra_notes', models.CharField(blank=True, default='', max_length=250)), + ('active', models.BooleanField(default=True)), + ('filters', models.ManyToManyField(blank=True, to='home.r_contentfilter')), + ('message_style', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='home.r_messagestyle')), + ('server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.r_server')), + ], + ), + migrations.CreateModel( + name='r_Content', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('item_id', models.CharField(max_length=1024)), + ('item_guid', models.CharField(max_length=1024)), + ('item_url', models.CharField(max_length=1024)), + ('item_title', models.CharField(max_length=1024)), + ('item_content_hash', models.CharField(max_length=1024)), + ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.r_subscription')), + ], + ), + ] diff --git a/apps/home/models.py b/apps/home/models.py index d7a07fd..702f531 100644 --- a/apps/home/models.py +++ b/apps/home/models.py @@ -1,12 +1,10 @@ -# -*- encoding: utf-8 -*- import logging -from pathlib import Path from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from django.core.validators import MaxValueValidator, MinValueValidator +from django.core.exceptions import ValidationError log = logging.getLogger(__name__) @@ -420,7 +418,7 @@ class UniqueContentRule(models.Model): #region Rewrite - - - - - - - - -class _Server(models.Model): +class r_Server(models.Model): id = models.PositiveIntegerField(primary_key=True) name = models.CharField(max_length=128) icon_hash = models.CharField(max_length=128) @@ -430,25 +428,13 @@ class _Server(models.Model): def icon_url(self): return f"https://cdn.discordapp.com/icons/{self.id}/{self.icon_hash}.webp?size=80" + def __str__(self): + return self.name -class _Subscription(models.Model): + +class r_ContentFilter(models.Model): id = models.AutoField(primary_key=True) - server = models.ForeignKey(to=_Server, on_delete=models.CASCADE) - - name = models.CharField(max_length=32, blank=False) - url = models.URLField() - created_at = models.DateTimeField(default=timezone.now, editable=False) - updated_at = models.DateTimeField(default=timezone.now) - extra_notes = models.CharField(max_length=250, default="", blank=True) - active = models.BooleanField(default=True) - - filters = models.ManyToManyField(to=_ContentFilter, blank=True) - message_style = models.ForeignKey(to=_MessageStyle, blank=False) - - -class _ContentFilter(models.Model): - id = models.AutoField(primary_key=True) - server = models.ForeignKey(to=_Server) + server = models.ForeignKey(to=r_Server, on_delete=models.CASCADE) MATCH_NONE = 0 MATCH_ANY = 1 @@ -473,22 +459,23 @@ class _ContentFilter(models.Model): is_insensitive = models.BooleanField() is_whitelist = models.BooleanField() + def __str__(self): + return self.name -class _Content(models.Model): + +# Instances of this model are predefined only +class r_MessageMutator(models.Model): id = models.AutoField(primary_key=True) - subscription = models.ForeignKey(to=_Subscription, on_delete=models.CASCADE) + name = models.CharField(max_length=64) + value = models.CharField(max_length=32) - # 'item_' prefix is to differentiate between the internal identifiers and the stored data - item_id = models.CharField(max_length=1024) - item_guid = models.CharField(max_length=1024) - item_url = models.CharField(max_length=1024) - item_title = models.CharField(max_length=1024) - item_content_hash = models.CharField(max_length=1024) + def __str__(self): + return self.name -class _MessageStyle(models.Model): +class r_MessageStyle(models.Model): id = models.AutoField(primary_key=True) - server = models.ForeignKey(to=_Server, on_delete=models.CASCADE) + server = models.ForeignKey(to=r_Server, on_delete=models.CASCADE) is_embed = models.BooleanField(default=True) is_hyperlinked = models.BooleanField() # title only @@ -497,29 +484,90 @@ class _MessageStyle(models.Model): show_images = models.BooleanField() fetch_images = models.BooleanField() # if not included with RSS item - title_mutator = models.ForeignKey(to=_MessageMutator, on_delete=models.SET_NULL) - description_mutator = models.ForeignKey(to=_MessageMutator, on_delete=models.SET_NULL) + title_mutator = models.ForeignKey( + to=r_MessageMutator, + related_name="title_mutated_messagestyle", + on_delete=models.SET_NULL, + null=True, + blank=True + ) + description_mutator = models.ForeignKey( + to=r_MessageMutator, + related_name="desc_mutated_messagestyle", + on_delete=models.SET_NULL, + null=True, + blank=True + ) + + def __str__(self): + return f"{self.server.name} - {self.id}" + + +class r_Subscription(models.Model): + id = models.AutoField(primary_key=True) + server = models.ForeignKey(to=r_Server, on_delete=models.CASCADE, blank=False) + + name = models.CharField(max_length=32, blank=False) + url = models.URLField() + created_at = models.DateTimeField(default=timezone.now, editable=False) + updated_at = models.DateTimeField(default=timezone.now) + extra_notes = models.CharField(max_length=250, default="", blank=True) + active = models.BooleanField(default=True) + + 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 + + +class r_Content(models.Model): + id = models.AutoField(primary_key=True) + subscription = models.ForeignKey(to=r_Subscription, on_delete=models.CASCADE) + + # 'item_' prefix is to differentiate between the internal identifiers and the stored data + item_id = models.CharField(max_length=1024) + item_guid = models.CharField(max_length=1024) + item_url = models.CharField(max_length=1024) + item_title = models.CharField(max_length=1024) + item_content_hash = models.CharField(max_length=1024) + + def __str__(self): + return f"{self.subscription.name} - {self.id}" # Instances of this model are predefined only -class _MessageMutator(models.Model): +class r_UniqueContentRule(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=64) value = models.CharField(max_length=32) - -# Instances of this model are predefined only -class _UniqueContentRule(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=64) - value = models.CharField(max_length=32) + def __str__(self): + return self.name # Relevant logs from the bot logic class BotLogicLogs(models.Model): id = models.AutoField(primary_key=True) - server = models.ForeignKey(to=_Server, on_delete=models.CASCADE) + server = models.ForeignKey(to=r_Server, on_delete=models.CASCADE) - level = models.CharField() - message = models.CharField() # todo + level = models.CharField(max_length=32) + message = models.CharField(max_length=256) created_at = models.DateTimeField(default=timezone.now, editable=False) + + def __str__(self): + return f"{self.server.name} - {self.id}"