All checks were successful
Build and Push Docker Image / build (push) Successful in 8s
set blank=False, meaning 400 error will be raised if the channels field is missing or blank.
299 lines
8.7 KiB
Python
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}"
|