PYRSS-Website/apps/home/models.py

385 lines
11 KiB
Python

# -*- 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