PYRSS-Website/apps/home/models.py
Corban-Lee Jones c5595ab823
All checks were successful
Build and Push Docker Image / build (push) Successful in 8s
subscription channels required
set blank=False, meaning 400 error will be raised if the channels field is missing or blank.
2024-10-14 23:29:16 +01:00

299 lines
8.7 KiB
Python

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.PositiveIntegerField(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'")
super().delete()
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 Unique Content Rule
class UniqueContentRule(models.Model):
"""
Definitions for what content should be unique
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 = "unique content rule"
verbose_name_plural = "unique content rules"
get_latest_by = "id"
def __str__(self):
return self.name
# 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.PositiveIntegerField(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=True)
unique_rules = models.ManyToManyField(to=UniqueContentRule, 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_content_hash = models.CharField(max_length=1024)
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}"