# -*- 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 log = logging.getLogger(__name__) class GuildSettings(models.Model): """ Represents settings for a saved Discord Guild `SavedGuild`. These objects aren't linked through foreignkey because `SavedGuild` is user user unique, not Discord Guild unique. """ id = models.AutoField(primary_key=True) guild_id = models.CharField( verbose_name=_("guild id"), max_length=128, help_text=_("Discord snowflake ID for the represented guild."), unique=True ) default_embed_colour = models.CharField( verbose_name=_("default embed colour"), max_length=6, default="3498db", blank=True ) active = models.BooleanField( verbose_name=_("Active"), default=True, help_text=_("Subscriptions of inactive guilds will also be treated as inactive") ) class Meta: """ Metadata for the GuildSettings model. """ verbose_name = "guild settings" verbose_name_plural = "guild settings" get_latest_by = "id" class SavedGuilds(models.Model): """ Represents a saved Discord Guild (aka Server). These are shown in the UI on the sidebar, and can be selected to see associated Subscriptions. """ id = models.AutoField(_("ID"), primary_key=True) # Have to use charfield instead of positiveBigIntegerField due to an Sqlite # issue that rounds down the value # https://github.com/sequelize/sequelize/issues/9335 guild_id = models.CharField( verbose_name=_("guild id"), max_length=128, help_text=_("Discord snowflake ID for the represented guild.") ) name = models.CharField( max_length=128, help_text=_("Name of the represented guild.") ) icon = models.CharField( max_length=128, help_text=_("Hash for the represented guild's icon.") ) added_by = models.ForeignKey( verbose_name=_("added by"), to="authentication.DiscordUser", on_delete=models.CASCADE, help_text=_("The user who added created this instance.") ) permissions = models.CharField( max_length=64, help_text=_("Guild permissions for the user who added this instance.") ) owner = models.BooleanField( default=False, help_text=_("Does the 'added by' user own this guild?") ) class Meta: """ Metadata for the SavedGuilds Model. """ verbose_name = "saved guild" verbose_name_plural = "saved guilds" get_latest_by = "-creation_datetime" constraints = [ # Prevent servers from having subscriptions with duplicate names models.UniqueConstraint( fields=["added_by", "guild_id"], name="unique added_by & guild_id pair" ) ] def __str__(self) -> str: return self.name @property def settings(self): return GuildSettings.objects.get(guild_id=self.guild_id) def save(self, *args, **kwargs): GuildSettings.objects.get_or_create(guild_id=self.guild_id) super().save(*args, **kwargs) class SubChannel(models.Model): """ Represents a Discord TextChannel, saved against a Subscription. SubChannels are used as targets to send content from Subscriptions. """ id = models.AutoField(primary_key=True) # Have to use charfield instead of positiveBigIntegerField due to an Sqlite # issue that rounds down the value # https://github.com/sequelize/sequelize/issues/9335 channel_id = models.CharField( verbose_name=_("channel id"), max_length=128, help_text=_("Discord snowflake ID for the represented Channel.") ) channel_name = models.CharField( verbose_name=_("channel name"), max_length=256, help_text=_("Name of the represented Channel.") ) subscription = models.ForeignKey( to="home.Subscription", on_delete=models.CASCADE, help_text=_("The linked Subscription, must be unique.") ) class Meta: """ Metadata for the SubChannel Model. """ verbose_name = "SubChannel" verbose_name_plural = "SubChannels" get_latest_by = "id" constraints = [ # Prevent servers from having subscriptions with duplicate names models.UniqueConstraint( fields=["channel_id", "subscription"], name="unique channel id and subscription pair") ] def __str__(self) -> str: return self.channel_id # using a brilliant matching model design from paperless-ngx src class Filter(models.Model): 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")), # (MATCH_AUTO, _("Automatic")), ) id = models.AutoField(primary_key=True) name = models.CharField(_("name"), max_length=128) match = models.CharField(_("match"), max_length=256, blank=True) matching_algorithm = models.PositiveIntegerField( _("matching algorithm"), choices=MATCHING_ALGORITHMS, default=MATCH_ANY, ) is_insensitive = models.BooleanField(_("is insensitive"), default=True) is_whitelist = models.BooleanField(_("is whitelist"), default=False) # Have to use charfield instead of positiveBigIntegerField due to an Sqlite # issue that rounds down the value # https://github.com/sequelize/sequelize/issues/9335 guild_id = models.CharField(_("guild id"), max_length=128) class Meta: ordering = ("name",) constraints = [ models.UniqueConstraint( fields=["name", "guild_id"], name="unique name & guild id pair" ) ] def __str__(self): return f"{self.guild_id} - {self.name}" class Subscription(models.Model): """ The Subscription Model. 'Subscription' in the context of PYRSS is an RSS Feed with various settings. """ id = models.AutoField(primary_key=True) name = models.CharField( _("Name"), max_length=32, null=False, blank=False ) url = models.URLField(_("URL")) # NOTE: # Have to use charfield instead of positiveBigIntegerField due to an Sqlite # issue that rounds down the value # https://github.com/sequelize/sequelize/issues/9335 guild_id = models.CharField( _("Guild ID"), max_length=128 ) creation_datetime = models.DateTimeField( _("Created At"), default=timezone.now, editable=False ) extra_notes = models.CharField( _("Extra Notes"), max_length=250, null=True, blank=True, ) filters = models.ManyToManyField(to="home.Filter", blank=True) article_title_mutators = models.ManyToManyField( to="home.ArticleMutator", related_name="title_mutated_subscriptions", blank=True, ) article_desc_mutators = models.ManyToManyField( to="home.ArticleMutator", related_name="desc_mutated_subscriptions", blank=True, ) embed_colour = models.CharField( _("Embed Colour"), max_length=6, default="3498db", blank=True ) published_threshold = models.DateTimeField(_("Published Threshold"), default=timezone.now, blank=True) article_fetch_image = models.BooleanField( _("Fetch Article Images"), default=True, help_text="Will the resulting article have an image?" ) active = models.BooleanField(_("Active"), default=True) class Meta: """ Metadata for the Subscription Model. """ verbose_name = "subscription" verbose_name_plural = "subscriptions" get_latest_by = "-creation_datetime" constraints = [ # Prevent servers from having subscriptions with duplicate names models.UniqueConstraint(fields=["name", "guild_id"], name="unique name & server pair") ] @property def channels_count(self) -> int: """ Returns the number of 'SubChannel' objects assocaited with this subscription. """ return len(SubChannel.objects.filter(subscription=self)) def save(self, *args, **kwargs): new_text = "New " if self._state.adding else "" log.debug("%sSubscription Saved %s", new_text, self.id) super().save(*args, **kwargs) def __str__(self) -> str: return self.name class TrackedContent(models.Model): """ Tracked Content Model 'Tracked Content' identifies articles and tracks them being sent. This is used to ensure duplicate articles aren't sent in feeds. """ id = models.AutoField(_("ID"), primary_key=True) guid = models.CharField( _("GUID"), max_length=256, help_text=_("RSS provided GUID of the content") ) title = models.CharField(_("Title"), max_length=728) url = models.URLField(_("URL")) subscription = models.ForeignKey(to=Subscription, on_delete=models.CASCADE) channel_id = models.CharField(_("Channel ID"), max_length=128) message_id = models.CharField(_("Message ID"), max_length=128) blocked = models.BooleanField(_("Blocked"), default=False) creation_datetime = models.DateTimeField( _("Created At"), default=timezone.now, editable=False ) class Meta: verbose_name = "tracked content" verbose_name = "tracked contents" get_latest_by = "-creation_datetime" constraints = [ models.UniqueConstraint(fields=["guid", "channel_id"], name="unique guid & channel_id pair"), models.UniqueConstraint(fields=["url", "channel_id"], name="unique url & channel_id pair") ] def __str__(self) -> str: return self.title class ArticleMutator(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=64) value = models.CharField(max_length=32) def __str__(self) -> str: return self.name