import logging from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError from django.db.models.signals import post_save from django.dispatch import receiver log = logging.getLogger(__name__) # region Server class Server(models.Model): """ Represents a Discord Server. Instances of this model are automatically handled, and manual intervension should be avoided if possible. """ id = models.PositiveBigIntegerField(primary_key=True) name = models.CharField(max_length=128) icon_hash = models.CharField(max_length=128, blank=True, null=True) is_bot_operational = models.BooleanField(default=None, null=True) active = models.BooleanField(default=True) class Meta: verbose_name = "server" verbose_name_plural = "servers" get_latest_by = "name" @property 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 # region Content Filter class ContentFilter(models.Model): """ Filters for the content produced by Subscriptions. Owned by the related server. """ id = models.AutoField(primary_key=True) server = models.ForeignKey(to=Server, on_delete=models.CASCADE) MATCH_NONE = 0 MATCH_ANY = 1 MATCH_ALL = 2 MATCH_LITERAL = 3 MATCH_REGEX = 4 MATCH_FUZZY = 5 MATCH_AUTO = 6 MATCHING_ALGORITHMS = ( (MATCH_NONE, _("None")), (MATCH_ANY, _("Any: Item contains any of these words (space separated)")), (MATCH_ALL, _("All: Item contains all of these words (space separated)")), (MATCH_LITERAL, _("Exact: Item contains this string")), (MATCH_REGEX, _("Regular expression: Item matches this regex")), (MATCH_FUZZY, _("Fuzzy: Item contains a word similar to this word")), ) name = models.CharField(max_length=32) match = models.CharField(max_length=256, blank=False) matching_algorithm = models.PositiveIntegerField(choices=MATCHING_ALGORITHMS) is_insensitive = models.BooleanField() is_whitelist = models.BooleanField() class Meta: verbose_name = "filter" verbose_name_plural = "filters" get_latest_by = "id" def __str__(self): return self.name # region Message Mutator class MessageMutator(models.Model): """ Mutators to be applied via the Bot. Instances of this model are predefined via migrations. Manual editing should be avoided at all costs! """ id = models.AutoField(primary_key=True) name = models.CharField(max_length=64) value = models.CharField(max_length=32) class Meta: verbose_name = "message mutator" verbose_name_plural = "message mutators" get_latest_by = "id" def __str__(self): return self.name # region Message Style class MessageStyle(models.Model): """ Custom styles to be applied via the Bot. Owned by the related server. """ id = models.AutoField(primary_key=True) server = models.ForeignKey(to=Server, on_delete=models.CASCADE, null=True, blank=False) name = models.CharField(max_length=32) colour = models.CharField(max_length=6, default="3498db") is_embed = models.BooleanField() is_hyperlinked = models.BooleanField() # title only show_author = models.BooleanField() show_timestamp = models.BooleanField() show_images = models.BooleanField() fetch_images = models.BooleanField() # if not included with RSS item title_mutator = models.ForeignKey( to=MessageMutator, related_name="title_mutated_messagestyle", on_delete=models.SET_NULL, null=True, blank=True ) description_mutator = models.ForeignKey( to=MessageMutator, related_name="desc_mutated_messagestyle", on_delete=models.SET_NULL, null=True, blank=True ) auto_created = models.BooleanField(default=False, blank=True) class Meta: verbose_name = "message style" verbose_name_plural = "message styles" get_latest_by = "id" def delete(self, *args, **kwargs): if self.auto_created: raise ValidationError("Cannot delete 'MessageStyle' instance with 'auto_created=True'") # If this style is being used, reset the users to the default style default_message_style = MessageStyle.objects.get(server=self.server, auto_created=True) Subscription.objects \ .filter(server=self.server, message_style=self) \ .update(message_style=default_message_style) super().delete(*args, **kwargs) def __str__(self): return self.name @receiver(post_save, sender=Server) def create_default_items(sender, instance, created, **kwargs): if not created: return # Create a default message style, so the user can get straight into creating subscriptions # (subscriptions require a message style to exist) MessageStyle.objects.create( server=instance, name=_("Default Message Style"), colour="3498db", is_embed=True, is_hyperlinked=True, show_author=True, show_timestamp=True, show_images=True, fetch_images=True, title_mutator=None, description_mutator=None, auto_created=True ) # region Discord Channel class DiscordChannel(models.Model): """ Store limited data on a relevant Channel from Discord, used to indicate where subscriptions should send content to. Instance creation & deletion is handled internally, when Subscriptions are modified. """ id = models.PositiveBigIntegerField(primary_key=True) server = models.ForeignKey(to=Server, on_delete=models.CASCADE, blank=False) name = models.CharField(max_length=128) is_nsfw = models.BooleanField() def __str__(self): return f"#{self.name}" # region Subscription class Subscription(models.Model): """ These represent RSSFeeds, storing relevant settings for managing them. Owned by the related server. """ id = models.AutoField(primary_key=True) server = models.ForeignKey(to=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) publish_threshold = models.DateTimeField(default=timezone.now) channels = models.ManyToManyField(to=DiscordChannel, related_name="subscriptions", blank=False) filters = models.ManyToManyField(to=ContentFilter, blank=True) message_style = models.ForeignKey(to=MessageStyle, on_delete=models.SET_NULL, null=True, blank=False) class Meta: verbose_name = "subscription" verbose_name_plural = "subscriptions" get_latest_by = "updated_at" def __str__(self): return self.name # region Content class Content(models.Model): """ Represents a processed item created from a Subscription. """ id = models.AutoField(primary_key=True) subscription = models.ForeignKey(to=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_description = models.CharField(max_length=1024) item_content_hash = models.CharField(max_length=1024) item_image_url = models.URLField(null=True, blank=True) item_thumbnail_url = models.URLField(null=True, blank=True) item_published = models.DateField(null=True, blank=True) item_author = models.CharField(max_length=256, null=True, blank=True) item_author_url = models.URLField(null=True, blank=True) item_feed_title = models.CharField(max_length=1024) item_feed_url = models.URLField() blocked = models.BooleanField(default=False) class Meta: verbose_name = "content" verbose_name_plural = "content" get_latest_by = "id" def __str__(self): return f"{self.subscription.name} - {self.id}" # region Bot Logic Logs # 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) level = models.CharField(max_length=32) message = models.CharField(max_length=256) created_at = models.DateTimeField(default=timezone.now, editable=False) class Meta: verbose_name = "bot logic log" verbose_name_plural = "bot logic logs" get_latest_by = "id" def __str__(self): return f"{self.server.name} - {self.id}" # Subscription Recommendations class SubscriptionRecommendation(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=32) description = models.CharField(max_length=250) url = models.URLField() def __str__(self): return self.url