diff options
Diffstat (limited to 'pydis_site/apps/api')
26 files changed, 679 insertions, 513 deletions
| diff --git a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py index ea8f03d2..d53ddb90 100644 --- a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py +++ b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py @@ -1,6 +1,6 @@  # Generated by Django 2.1.1 on 2018-09-23 10:07 -import pydis_site.apps.api.validators +import pydis_site.apps.api.models.bot.tag  import django.contrib.postgres.fields.jsonb  from django.db import migrations @@ -15,6 +15,6 @@ class Migration(migrations.Migration):          migrations.AlterField(              model_name='tag',              name='embed', -            field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[pydis_site.apps.api.validators.validate_tag_embed]), +            field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]),          ),      ] diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py index f451ecf4..4b028f0c 100644 --- a/pydis_site/apps/api/migrations/0019_deletedmessage.py +++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py @@ -1,7 +1,6 @@  # Generated by Django 2.1.1 on 2018-11-18 20:26  import pydis_site.apps.api.models -import pydis_site.apps.api.validators  from django.db import migrations, models  import django.db.models.deletion @@ -19,7 +18,7 @@ class Migration(migrations.Migration):                  ('id', models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')])),                  ('channel_id', models.BigIntegerField(help_text='The channel ID that this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])),                  ('content', models.CharField(help_text='The content of this message, taken from Discord.', max_length=2000)), -                ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.validators.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)), +                ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)),                  ('author', models.ForeignKey(help_text='The author of this message.', on_delete=django.db.models.deletion.CASCADE, to='api.User')),                  ('deletion_context', models.ForeignKey(help_text='The deletion context this message is part of.', on_delete=django.db.models.deletion.CASCADE, to='api.MessageDeletionContext')),              ], diff --git a/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py b/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py index bd370d8e..d2a98e5d 100644 --- a/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py +++ b/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py @@ -1,6 +1,6 @@  # Generated by Django 2.1.5 on 2019-02-18 19:41 -import pydis_site.apps.api.validators +import pydis_site.apps.api.models.bot.bot_setting  from django.db import migrations, models @@ -15,6 +15,6 @@ class Migration(migrations.Migration):              model_name='botsetting',              name='name',              field=models.CharField(max_length=50, primary_key=True, serialize=False, validators=[ -                pydis_site.apps.api.validators.validate_bot_setting_name]), +                pydis_site.apps.api.models.bot.bot_setting.validate_bot_setting_name]),          ),      ] diff --git a/pydis_site/apps/api/models.py b/pydis_site/apps/api/models.py deleted file mode 100644 index b2499f8d..00000000 --- a/pydis_site/apps/api/models.py +++ /dev/null @@ -1,498 +0,0 @@ -from operator import itemgetter - -from django.contrib.postgres import fields as pgfields -from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator -from django.db import models -from django.utils import timezone - -from .validators import validate_bot_setting_name, validate_tag_embed - - -class ModelReprMixin: -    """ -    Adds a `__repr__` method to the model subclassing this -    mixin which will display the model's class name along -    with all parameters used to construct the object. -    """ - -    def __repr__(self): -        attributes = ' '.join( -            f'{attribute}={value!r}' -            for attribute, value in sorted( -                self.__dict__.items(), -                key=itemgetter(0) -            ) -            if not attribute.startswith('_') -        ) -        return f'<{self.__class__.__name__}({attributes})>' - - -class BotSetting(ModelReprMixin, models.Model): -    """A configuration entry for the bot.""" - -    name = models.CharField( -        primary_key=True, -        max_length=50, -        validators=(validate_bot_setting_name,) -    ) -    data = pgfields.JSONField( -        help_text="The actual settings of this setting." -    ) - - -class DocumentationLink(ModelReprMixin, models.Model): -    """A documentation link used by the `!docs` command of the bot.""" - -    package = models.CharField( -        primary_key=True, -        max_length=50, -        help_text="The Python package name that this documentation link belongs to." -    ) -    base_url = models.URLField( -        help_text=( -            "The base URL from which documentation will be available for this project. " -            "Used to generate links to various symbols within this package." -        ) -    ) -    inventory_url = models.URLField( -        help_text="The URL at which the Sphinx inventory is available for this package." -    ) - -    def __str__(self): -        return f"{self.package} - {self.base_url}" - - -class OffTopicChannelName(ModelReprMixin, models.Model): -    name = models.CharField( -        primary_key=True, -        max_length=96, -        validators=(RegexValidator(regex=r'^[a-z0-9-]+$'),), -        help_text="The actual channel name that will be used on our Discord server." -    ) - -    def __str__(self): -        return self.name - - -class Role(ModelReprMixin, models.Model): -    """A role on our Discord server.""" - -    id = models.BigIntegerField(  # noqa -        primary_key=True, -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Role IDs cannot be negative." -            ), -        ), -        help_text="The role ID, taken from Discord." -    ) -    name = models.CharField( -        max_length=100, -        help_text="The role name, taken from Discord." -    ) -    colour = models.IntegerField( -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Colour hex cannot be negative." -            ), -        ), -        help_text="The integer value of the colour of this role from Discord." -    ) -    permissions = models.IntegerField( -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Role permissions cannot be negative." -            ), -            MaxValueValidator( -                limit_value=2 << 32, -                message="Role permission bitset exceeds value of having all permissions" -            ) -        ), -        help_text="The integer value of the permission bitset of this role from Discord." -    ) - -    def __str__(self): -        return self.name - - -class SnakeFact(ModelReprMixin, models.Model): -    """A snake fact used by the bot's snake cog.""" - -    fact = models.CharField( -        primary_key=True, -        max_length=200, -        help_text="A fact about snakes." -    ) - -    def __str__(self): -        return self.fact - - -class SnakeIdiom(ModelReprMixin, models.Model): -    """A snake idiom used by the snake cog.""" - -    idiom = models.CharField( -        primary_key=True, -        max_length=140, -        help_text="A saying about a snake." -    ) - -    def __str__(self): -        return self.idiom - - -class SnakeName(ModelReprMixin, models.Model): -    """A snake name used by the bot's snake cog.""" - -    name = models.CharField( -        primary_key=True, -        max_length=100, -        help_text="The regular name for this snake, e.g. 'Python'.", -        validators=[RegexValidator(regex=r'^([^0-9])+$')] -    ) -    scientific = models.CharField( -        max_length=150, -        help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", -        validators=[RegexValidator(regex=r'^([^0-9])+$')] -    ) - -    def __str__(self): -        return f"{self.name} ({self.scientific})" - - -class SpecialSnake(ModelReprMixin, models.Model): -    """A special snake's name, info and image from our database used by the bot's snake cog.""" - -    name = models.CharField( -        max_length=140, -        primary_key=True, -        help_text='A special snake name.', -        validators=[RegexValidator(regex=r'^([^0-9])+$')] -    ) -    info = models.TextField( -        help_text='Info about a special snake.' -    ) -    images = pgfields.ArrayField( -        models.URLField(), -        help_text='Images displaying this special snake.' -    ) - -    def __str__(self): -        return self.name - - -class Tag(ModelReprMixin, models.Model): -    """A tag providing (hopefully) useful information.""" - -    title = models.CharField( -        max_length=100, -        help_text=( -            "The title of this tag, shown in searches and providing " -            "a quick overview over what this embed contains." -        ), -        primary_key=True -    ) -    embed = pgfields.JSONField( -        help_text="The actual embed shown by this tag.", -        validators=(validate_tag_embed,) -    ) - -    def __str__(self): -        return self.title - - -class User(ModelReprMixin, models.Model): -    """A Discord user.""" - -    id = models.BigIntegerField(  # noqa -        primary_key=True, -        validators=( -            MinValueValidator( -                limit_value=0, -                message="User IDs cannot be negative." -            ), -        ), -        help_text="The ID of this user, taken from Discord." -    ) -    name = models.CharField( -        max_length=32, -        help_text="The username, taken from Discord." -    ) -    discriminator = models.PositiveSmallIntegerField( -        validators=( -            MaxValueValidator( -                limit_value=9999, -                message="Discriminators may not exceed `9999`." -            ), -        ), -        help_text="The discriminator of this user, taken from Discord." -    ) -    avatar_hash = models.CharField( -        max_length=100, -        help_text=( -            "The user's avatar hash, taken from Discord. " -            "Null if the user does not have any custom avatar." -        ), -        null=True -    ) -    roles = models.ManyToManyField( -        Role, -        help_text="Any roles this user has on our server." -    ) -    in_guild = models.BooleanField( -        default=True, -        help_text="Whether this user is in our server." -    ) - -    def __str__(self): -        return f"{self.name}#{self.discriminator}" - - -class Message(ModelReprMixin, models.Model): -    id = models.BigIntegerField( -        primary_key=True, -        help_text="The message ID as taken from Discord.", -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Message IDs cannot be negative." -            ), -        ) -    ) -    author = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        help_text="The author of this message." -    ) -    channel_id = models.BigIntegerField( -        help_text=( -            "The channel ID that this message was " -            "sent in, taken from Discord." -        ), -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Channel IDs cannot be negative." -            ), -        ) -    ) -    content = models.CharField( -        max_length=2_000, -        help_text="The content of this message, taken from Discord.", -        blank=True -    ) -    embeds = pgfields.ArrayField( -        pgfields.JSONField( -            validators=(validate_tag_embed,) -        ), -        help_text="Embeds attached to this message." -    ) - -    class Meta: -        abstract = True - - -class MessageDeletionContext(ModelReprMixin, models.Model): -    actor = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        help_text=( -            "The original actor causing this deletion. Could be the author " -            "of a manual clean command invocation, the bot when executing " -            "automatic actions, or nothing to indicate that the bulk " -            "deletion was not issued by us." -        ), -        null=True -    ) -    creation = models.DateTimeField( -        # Consider whether we want to add a validator here that ensures -        # the deletion context does not take place in the future. -        help_text="When this deletion took place." -    ) - - -class DeletedMessage(Message): -    deletion_context = models.ForeignKey( -        MessageDeletionContext, -        help_text="The deletion context this message is part of.", -        on_delete=models.CASCADE -    ) - - -class Infraction(ModelReprMixin, models.Model): -    """An infraction for a Discord user.""" - -    TYPE_CHOICES = ( -        ("note", "Note"), -        ("warning", "Warning"), -        ("watch", "Watch"), -        ("mute", "Mute"), -        ("kick", "Kick"), -        ("ban", "Ban"), -        ("superstar", "Superstar") -    ) -    inserted_at = models.DateTimeField( -        default=timezone.now, -        help_text="The date and time of the creation of this infraction." -    ) -    expires_at = models.DateTimeField( -        null=True, -        help_text=( -            "The date and time of the expiration of this infraction. " -            "Null if the infraction is permanent or it can't expire." -        ) -    ) -    active = models.BooleanField( -        default=True, -        help_text="Whether the infraction is still active." -    ) -    user = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        related_name='infractions_received', -        help_text="The user to which the infraction was applied." -    ) -    actor = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        related_name='infractions_given', -        help_text="The user which applied the infraction." -    ) -    type = models.CharField( -        max_length=9, -        choices=TYPE_CHOICES, -        help_text="The type of the infraction." -    ) -    reason = models.TextField( -        null=True, -        help_text="The reason for the infraction." -    ) -    hidden = models.BooleanField( -        default=False, -        help_text="Whether the infraction is a shadow infraction." -    ) - -    def __str__(self): -        s = f"#{self.id}: {self.type} on {self.user_id}" -        if self.expires_at: -            s += f" until {self.expires_at}" -        if self.hidden: -            s += " (hidden)" -        return s - - -class Reminder(ModelReprMixin, models.Model): -    """A reminder created by a user.""" - -    active = models.BooleanField( -        default=True, -        help_text=( -            "Whether this reminder is still active. " -            "If not, it has been sent out to the user." -        ) -    ) -    author = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        help_text="The creator of this reminder." -    ) -    channel_id = models.BigIntegerField( -        help_text=( -            "The channel ID that this message was " -            "sent in, taken from Discord." -        ), -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Channel IDs cannot be negative." -            ), -        ) -    ) -    content = models.CharField( -        max_length=1500, -        help_text="The content that the user wants to be reminded of." -    ) -    expiration = models.DateTimeField( -        help_text="When this reminder should be sent." -    ) - -    def __str__(self): -        return f"{self.content} on {self.expiration} by {self.author}" - - -class Nomination(ModelReprMixin, models.Model): -    """A helper nomination created by staff.""" - -    active = models.BooleanField( -        default=True, -        help_text="Whether this nomination is still relevant." -    ) -    author = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        help_text="The staff member that nominated this user.", -        related_name='nomination_set' -    ) -    reason = models.TextField( -        help_text="Why this user was nominated." -    ) -    user = models.OneToOneField( -        User, -        on_delete=models.CASCADE, -        help_text="The nominated user.", -        primary_key=True, -        related_name='nomination' -    ) -    inserted_at = models.DateTimeField( -        auto_now_add=True, -        help_text="The creation date of this nomination." -    ) - - -class LogEntry(ModelReprMixin, models.Model): -    """A log entry generated by one of the PyDis applications.""" - -    application = models.CharField( -        max_length=20, -        help_text="The application that generated this log entry.", -        choices=( -            ('bot', 'Bot'), -            ('seasonalbot', 'Seasonalbot'), -            ('site', 'Website') -        ) -    ) -    logger_name = models.CharField( -        max_length=100, -        help_text="The name of the logger that generated this log entry." -    ) -    timestamp = models.DateTimeField( -        default=timezone.now, -        help_text="The date and time when this entry was created." -    ) -    level = models.CharField( -        max_length=8,  # 'critical' -        choices=( -            ('debug', 'Debug'), -            ('info', 'Info'), -            ('warning', 'Warning'), -            ('error', 'Error'), -            ('critical', 'Critical') -        ), -        help_text=( -            "The logger level at which this entry was emitted. The levels " -            "correspond to the Python `logging` levels." -        ) -    ) -    module = models.CharField( -        max_length=100, -        help_text="The fully qualified path of the module generating this log line." -    ) -    line = models.PositiveSmallIntegerField( -        help_text="The line at which the log line was emitted." -    ) -    message = models.TextField( -        help_text="The textual content of the log line." -    ) diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py new file mode 100644 index 00000000..4645bda2 --- /dev/null +++ b/pydis_site/apps/api/models/__init__.py @@ -0,0 +1,20 @@ +from .bot import (  # noqa +    BotSetting, +    DocumentationLink, +    DeletedMessage, +    Infraction, +    Message, +    MessageDeletionContext, +    Nomination, +    OffTopicChannelName, +    Reminder, +    Role, +    SnakeFact, +    SnakeIdiom, +    SnakeName, +    SpecialSnake, +    Tag, +    User +) +from .log_entry import LogEntry  # noqa +from .utils import ModelReprMixin  # noqa diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py new file mode 100644 index 00000000..fb313193 --- /dev/null +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -0,0 +1,16 @@ +from .bot_setting import BotSetting  # noqa +from .deleted_message import DeletedMessage  # noqa +from .documentation_link import DocumentationLink  # noqa +from .infraction import Infraction  # noqa +from .message import Message  # noqa +from .message_deletion_context import MessageDeletionContext  # noqa +from .nomination import Nomination  # noqa +from .off_topic_channel_name import OffTopicChannelName  # noqa +from .reminder import Reminder  # noqa +from .role import Role  # noqa +from .snake_fact import SnakeFact  # noqa +from .snake_idiom import SnakeIdiom  # noqa +from .snake_name import SnakeName  # noqa +from .special_snake import SpecialSnake  # noqa +from .tag import Tag  # noqa +from .user import User  # noqa diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py new file mode 100644 index 00000000..a6eeaa1f --- /dev/null +++ b/pydis_site/apps/api/models/bot/bot_setting.py @@ -0,0 +1,27 @@ +from django.contrib.postgres import fields as pgfields +from django.core.exceptions import ValidationError +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +def validate_bot_setting_name(name): +    known_settings = ( +        'defcon', +    ) + +    if name not in known_settings: +        raise ValidationError(f"`{name}` is not a known setting name.") + + +class BotSetting(ModelReprMixin, models.Model): +    """A configuration entry for the bot.""" + +    name = models.CharField( +        primary_key=True, +        max_length=50, +        validators=(validate_bot_setting_name,) +    ) +    data = pgfields.JSONField( +        help_text="The actual settings of this setting." +    ) diff --git a/pydis_site/apps/api/models/bot/deleted_message.py b/pydis_site/apps/api/models/bot/deleted_message.py new file mode 100644 index 00000000..0f46cd12 --- /dev/null +++ b/pydis_site/apps/api/models/bot/deleted_message.py @@ -0,0 +1,12 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.message import Message +from pydis_site.apps.api.models.bot.message_deletion_context import MessageDeletionContext + + +class DeletedMessage(Message): +    deletion_context = models.ForeignKey( +        MessageDeletionContext, +        help_text="The deletion context this message is part of.", +        on_delete=models.CASCADE +    ) diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py new file mode 100644 index 00000000..d7df22ad --- /dev/null +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -0,0 +1,25 @@ +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class DocumentationLink(ModelReprMixin, models.Model): +    """A documentation link used by the `!docs` command of the bot.""" + +    package = models.CharField( +        primary_key=True, +        max_length=50, +        help_text="The Python package name that this documentation link belongs to." +    ) +    base_url = models.URLField( +        help_text=( +            "The base URL from which documentation will be available for this project. " +            "Used to generate links to various symbols within this package." +        ) +    ) +    inventory_url = models.URLField( +        help_text="The URL at which the Sphinx inventory is available for this package." +    ) + +    def __str__(self): +        return f"{self.package} - {self.base_url}" diff --git a/pydis_site/apps/api/models/bot/infraction 3.py b/pydis_site/apps/api/models/bot/infraction 3.py new file mode 100644 index 00000000..76d7b881 --- /dev/null +++ b/pydis_site/apps/api/models/bot/infraction 3.py @@ -0,0 +1,67 @@ +from django.db import models +from django.utils import timezone + +from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.bot.user import User + + +class Infraction(ModelReprMixin, models.Model): +    """An infraction for a Discord user.""" + +    TYPE_CHOICES = ( +        ("note", "Note"), +        ("warning", "Warning"), +        ("watch", "Watch"), +        ("mute", "Mute"), +        ("kick", "Kick"), +        ("ban", "Ban"), +        ("superstar", "Superstar") +    ) +    inserted_at = models.DateTimeField( +        default=timezone.now, +        help_text="The date and time of the creation of this infraction." +    ) +    expires_at = models.DateTimeField( +        null=True, +        help_text=( +            "The date and time of the expiration of this infraction. " +            "Null if the infraction is permanent or it can't expire." +        ) +    ) +    active = models.BooleanField( +        default=True, +        help_text="Whether the infraction is still active." +    ) +    user = models.ForeignKey( +        User, +        on_delete=models.CASCADE, +        related_name='infractions_received', +        help_text="The user to which the infraction was applied." +    ) +    actor = models.ForeignKey( +        User, +        on_delete=models.CASCADE, +        related_name='infractions_given', +        help_text="The user which applied the infraction." +    ) +    type = models.CharField( +        max_length=9, +        choices=TYPE_CHOICES, +        help_text="The type of the infraction." +    ) +    reason = models.TextField( +        null=True, +        help_text="The reason for the infraction." +    ) +    hidden = models.BooleanField( +        default=False, +        help_text="Whether the infraction is a shadow infraction." +    ) + +    def __str__(self): +        s = f"#{self.id}: {self.type} on {self.user_id}" +        if self.expires_at: +            s += f" until {self.expires_at}" +        if self.hidden: +            s += " (hidden)" +        return s diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py new file mode 100644 index 00000000..911ca589 --- /dev/null +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -0,0 +1,67 @@ +from django.db import models +from django.utils import timezone + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Infraction(ModelReprMixin, models.Model): +    """An infraction for a Discord user.""" + +    TYPE_CHOICES = ( +        ("note", "Note"), +        ("warning", "Warning"), +        ("watch", "Watch"), +        ("mute", "Mute"), +        ("kick", "Kick"), +        ("ban", "Ban"), +        ("superstar", "Superstar") +    ) +    inserted_at = models.DateTimeField( +        default=timezone.now, +        help_text="The date and time of the creation of this infraction." +    ) +    expires_at = models.DateTimeField( +        null=True, +        help_text=( +            "The date and time of the expiration of this infraction. " +            "Null if the infraction is permanent or it can't expire." +        ) +    ) +    active = models.BooleanField( +        default=True, +        help_text="Whether the infraction is still active." +    ) +    user = models.ForeignKey( +        User, +        on_delete=models.CASCADE, +        related_name='infractions_received', +        help_text="The user to which the infraction was applied." +    ) +    actor = models.ForeignKey( +        User, +        on_delete=models.CASCADE, +        related_name='infractions_given', +        help_text="The user which applied the infraction." +    ) +    type = models.CharField( +        max_length=9, +        choices=TYPE_CHOICES, +        help_text="The type of the infraction." +    ) +    reason = models.TextField( +        null=True, +        help_text="The reason for the infraction." +    ) +    hidden = models.BooleanField( +        default=False, +        help_text="Whether the infraction is a shadow infraction." +    ) + +    def __str__(self): +        s = f"#{self.id}: {self.type} on {self.user_id}" +        if self.expires_at: +            s += f" until {self.expires_at}" +        if self.hidden: +            s += " (hidden)" +        return s diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py new file mode 100644 index 00000000..22500be0 --- /dev/null +++ b/pydis_site/apps/api/models/bot/message.py @@ -0,0 +1,51 @@ +from django.contrib.postgres import fields as pgfields +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.bot.tag import validate_tag_embed +from pydis_site.apps.api.models.bot.user import User + + +class Message(ModelReprMixin, models.Model): +    id = models.BigIntegerField( +        primary_key=True, +        help_text="The message ID as taken from Discord.", +        validators=( +            MinValueValidator( +                limit_value=0, +                message="Message IDs cannot be negative." +            ), +        ) +    ) +    author = models.ForeignKey( +        User, +        on_delete=models.CASCADE, +        help_text="The author of this message." +    ) +    channel_id = models.BigIntegerField( +        help_text=( +            "The channel ID that this message was " +            "sent in, taken from Discord." +        ), +        validators=( +            MinValueValidator( +                limit_value=0, +                message="Channel IDs cannot be negative." +            ), +        ) +    ) +    content = models.CharField( +        max_length=2_000, +        help_text="The content of this message, taken from Discord.", +        blank=True +    ) +    embeds = pgfields.ArrayField( +        pgfields.JSONField( +            validators=(validate_tag_embed,) +        ), +        help_text="Embeds attached to this message." +    ) + +    class Meta: +        abstract = True diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py new file mode 100644 index 00000000..9904ef71 --- /dev/null +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -0,0 +1,23 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class MessageDeletionContext(ModelReprMixin, models.Model): +    actor = models.ForeignKey( +        User, +        on_delete=models.CASCADE, +        help_text=( +            "The original actor causing this deletion. Could be the author " +            "of a manual clean command invocation, the bot when executing " +            "automatic actions, or nothing to indicate that the bulk " +            "deletion was not issued by us." +        ), +        null=True +    ) +    creation = models.DateTimeField( +        # Consider whether we want to add a validator here that ensures +        # the deletion context does not take place in the future. +        help_text="When this deletion took place." +    ) diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py new file mode 100644 index 00000000..5ebb9759 --- /dev/null +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -0,0 +1,33 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Nomination(ModelReprMixin, models.Model): +    """A helper nomination created by staff.""" + +    active = models.BooleanField( +        default=True, +        help_text="Whether this nomination is still relevant." +    ) +    author = models.ForeignKey( +        User, +        on_delete=models.CASCADE, +        help_text="The staff member that nominated this user.", +        related_name='nomination_set' +    ) +    reason = models.TextField( +        help_text="Why this user was nominated." +    ) +    user = models.OneToOneField( +        User, +        on_delete=models.CASCADE, +        help_text="The nominated user.", +        primary_key=True, +        related_name='nomination' +    ) +    inserted_at = models.DateTimeField( +        auto_now_add=True, +        help_text="The creation date of this nomination." +    ) diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py new file mode 100644 index 00000000..dff7eaf8 --- /dev/null +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -0,0 +1,16 @@ +from django.core.validators import RegexValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class OffTopicChannelName(ModelReprMixin, models.Model): +    name = models.CharField( +        primary_key=True, +        max_length=96, +        validators=(RegexValidator(regex=r'^[a-z0-9-]+$'),), +        help_text="The actual channel name that will be used on our Discord server." +    ) + +    def __str__(self): +        return self.name diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py new file mode 100644 index 00000000..abccdf82 --- /dev/null +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -0,0 +1,44 @@ +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Reminder(ModelReprMixin, models.Model): +    """A reminder created by a user.""" + +    active = models.BooleanField( +        default=True, +        help_text=( +            "Whether this reminder is still active. " +            "If not, it has been sent out to the user." +        ) +    ) +    author = models.ForeignKey( +        User, +        on_delete=models.CASCADE, +        help_text="The creator of this reminder." +    ) +    channel_id = models.BigIntegerField( +        help_text=( +            "The channel ID that this message was " +            "sent in, taken from Discord." +        ), +        validators=( +            MinValueValidator( +                limit_value=0, +                message="Channel IDs cannot be negative." +            ), +        ) +    ) +    content = models.CharField( +        max_length=1500, +        help_text="The content that the user wants to be reminded of." +    ) +    expiration = models.DateTimeField( +        help_text="When this reminder should be sent." +    ) + +    def __str__(self): +        return f"{self.content} on {self.expiration} by {self.author}" diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py new file mode 100644 index 00000000..8106394f --- /dev/null +++ b/pydis_site/apps/api/models/bot/role.py @@ -0,0 +1,48 @@ +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class Role(ModelReprMixin, models.Model): +    """A role on our Discord server.""" + +    id = models.BigIntegerField(  # noqa +        primary_key=True, +        validators=( +            MinValueValidator( +                limit_value=0, +                message="Role IDs cannot be negative." +            ), +        ), +        help_text="The role ID, taken from Discord." +    ) +    name = models.CharField( +        max_length=100, +        help_text="The role name, taken from Discord." +    ) +    colour = models.IntegerField( +        validators=( +            MinValueValidator( +                limit_value=0, +                message="Colour hex cannot be negative." +            ), +        ), +        help_text="The integer value of the colour of this role from Discord." +    ) +    permissions = models.IntegerField( +        validators=( +            MinValueValidator( +                limit_value=0, +                message="Role permissions cannot be negative." +            ), +            MaxValueValidator( +                limit_value=2 << 32, +                message="Role permission bitset exceeds value of having all permissions" +            ) +        ), +        help_text="The integer value of the permission bitset of this role from Discord." +    ) + +    def __str__(self): +        return self.name diff --git a/pydis_site/apps/api/models/bot/snake_fact.py b/pydis_site/apps/api/models/bot/snake_fact.py new file mode 100644 index 00000000..4398620a --- /dev/null +++ b/pydis_site/apps/api/models/bot/snake_fact.py @@ -0,0 +1,16 @@ +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SnakeFact(ModelReprMixin, models.Model): +    """A snake fact used by the bot's snake cog.""" + +    fact = models.CharField( +        primary_key=True, +        max_length=200, +        help_text="A fact about snakes." +    ) + +    def __str__(self): +        return self.fact diff --git a/pydis_site/apps/api/models/bot/snake_idiom.py b/pydis_site/apps/api/models/bot/snake_idiom.py new file mode 100644 index 00000000..e4db00e0 --- /dev/null +++ b/pydis_site/apps/api/models/bot/snake_idiom.py @@ -0,0 +1,16 @@ +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SnakeIdiom(ModelReprMixin, models.Model): +    """A snake idiom used by the snake cog.""" + +    idiom = models.CharField( +        primary_key=True, +        max_length=140, +        help_text="A saying about a snake." +    ) + +    def __str__(self): +        return self.idiom diff --git a/pydis_site/apps/api/models/bot/snake_name.py b/pydis_site/apps/api/models/bot/snake_name.py new file mode 100644 index 00000000..045a7faa --- /dev/null +++ b/pydis_site/apps/api/models/bot/snake_name.py @@ -0,0 +1,23 @@ +from django.core.validators import RegexValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SnakeName(ModelReprMixin, models.Model): +    """A snake name used by the bot's snake cog.""" + +    name = models.CharField( +        primary_key=True, +        max_length=100, +        help_text="The regular name for this snake, e.g. 'Python'.", +        validators=[RegexValidator(regex=r'^([^0-9])+$')] +    ) +    scientific = models.CharField( +        max_length=150, +        help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", +        validators=[RegexValidator(regex=r'^([^0-9])+$')] +    ) + +    def __str__(self): +        return f"{self.name} ({self.scientific})" diff --git a/pydis_site/apps/api/models/bot/special_snake.py b/pydis_site/apps/api/models/bot/special_snake.py new file mode 100644 index 00000000..1406b9e0 --- /dev/null +++ b/pydis_site/apps/api/models/bot/special_snake.py @@ -0,0 +1,26 @@ +from django.contrib.postgres import fields as pgfields +from django.core.validators import RegexValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class SpecialSnake(ModelReprMixin, models.Model): +    """A special snake's name, info and image from our database used by the bot's snake cog.""" + +    name = models.CharField( +        max_length=140, +        primary_key=True, +        help_text='A special snake name.', +        validators=[RegexValidator(regex=r'^([^0-9])+$')] +    ) +    info = models.TextField( +        help_text='Info about a special snake.' +    ) +    images = pgfields.ArrayField( +        models.URLField(), +        help_text='Images displaying this special snake.' +    ) + +    def __str__(self): +        return self.name diff --git a/pydis_site/apps/api/validators.py b/pydis_site/apps/api/models/bot/tag.py index ea2112a9..62881ca2 100644 --- a/pydis_site/apps/api/validators.py +++ b/pydis_site/apps/api/models/bot/tag.py @@ -1,7 +1,11 @@  from collections.abc import Mapping +from django.contrib.postgres import fields as pgfields  from django.core.exceptions import ValidationError  from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin  def validate_tag_embed_fields(fields): @@ -155,10 +159,21 @@ def validate_tag_embed(embed):                  validator(value) -def validate_bot_setting_name(name): -    known_settings = ( -        'defcon', +class Tag(ModelReprMixin, models.Model): +    """A tag providing (hopefully) useful information.""" + +    title = models.CharField( +        max_length=100, +        help_text=( +            "The title of this tag, shown in searches and providing " +            "a quick overview over what this embed contains." +        ), +        primary_key=True +    ) +    embed = pgfields.JSONField( +        help_text="The actual embed shown by this tag.", +        validators=(validate_tag_embed,)      ) -    if name not in known_settings: -        raise ValidationError(f"`{name}` is not a known setting name.") +    def __str__(self): +        return self.title diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py new file mode 100644 index 00000000..f5365ed1 --- /dev/null +++ b/pydis_site/apps/api/models/bot/user.py @@ -0,0 +1,52 @@ +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.bot.role import Role +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class User(ModelReprMixin, models.Model): +    """A Discord user.""" + +    id = models.BigIntegerField(  # noqa +        primary_key=True, +        validators=( +            MinValueValidator( +                limit_value=0, +                message="User IDs cannot be negative." +            ), +        ), +        help_text="The ID of this user, taken from Discord." +    ) +    name = models.CharField( +        max_length=32, +        help_text="The username, taken from Discord." +    ) +    discriminator = models.PositiveSmallIntegerField( +        validators=( +            MaxValueValidator( +                limit_value=9999, +                message="Discriminators may not exceed `9999`." +            ), +        ), +        help_text="The discriminator of this user, taken from Discord." +    ) +    avatar_hash = models.CharField( +        max_length=100, +        help_text=( +            "The user's avatar hash, taken from Discord. " +            "Null if the user does not have any custom avatar." +        ), +        null=True +    ) +    roles = models.ManyToManyField( +        Role, +        help_text="Any roles this user has on our server." +    ) +    in_guild = models.BooleanField( +        default=True, +        help_text="Whether this user is in our server." +    ) + +    def __str__(self): +        return f"{self.name}#{self.discriminator}" diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py new file mode 100644 index 00000000..acd7953a --- /dev/null +++ b/pydis_site/apps/api/models/log_entry.py @@ -0,0 +1,50 @@ +from django.db import models +from django.utils import timezone + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +class LogEntry(ModelReprMixin, models.Model): +    """A log entry generated by one of the PyDis applications.""" + +    application = models.CharField( +        max_length=20, +        help_text="The application that generated this log entry.", +        choices=( +            ('bot', 'Bot'), +            ('seasonalbot', 'Seasonalbot'), +            ('site', 'Website') +        ) +    ) +    logger_name = models.CharField( +        max_length=100, +        help_text="The name of the logger that generated this log entry." +    ) +    timestamp = models.DateTimeField( +        default=timezone.now, +        help_text="The date and time when this entry was created." +    ) +    level = models.CharField( +        max_length=8,  # 'critical' +        choices=( +            ('debug', 'Debug'), +            ('info', 'Info'), +            ('warning', 'Warning'), +            ('error', 'Error'), +            ('critical', 'Critical') +        ), +        help_text=( +            "The logger level at which this entry was emitted. The levels " +            "correspond to the Python `logging` levels." +        ) +    ) +    module = models.CharField( +        max_length=100, +        help_text="The fully qualified path of the module generating this log line." +    ) +    line = models.PositiveSmallIntegerField( +        help_text="The line at which the log line was emitted." +    ) +    message = models.TextField( +        help_text="The textual content of the log line." +    ) diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py new file mode 100644 index 00000000..731486e7 --- /dev/null +++ b/pydis_site/apps/api/models/utils.py @@ -0,0 +1,20 @@ +from operator import itemgetter + + +class ModelReprMixin: +    """ +    Adds a `__repr__` method to the model subclassing this +    mixin which will display the model's class name along +    with all parameters used to construct the object. +    """ + +    def __repr__(self): +        attributes = ' '.join( +            f'{attribute}={value!r}' +            for attribute, value in sorted( +                self.__dict__.items(), +                key=itemgetter(0) +            ) +            if not attribute.startswith('_') +        ) +        return f'<{self.__class__.__name__}({attributes})>' diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py index d0b78c23..ffa2f61e 100644 --- a/pydis_site/apps/api/tests/test_validators.py +++ b/pydis_site/apps/api/tests/test_validators.py @@ -1,10 +1,8 @@  from django.core.exceptions import ValidationError  from django.test import TestCase -from ..validators import ( -    validate_bot_setting_name, -    validate_tag_embed -) +from ..models.bot.bot_setting import validate_bot_setting_name +from ..models.bot.tag import validate_tag_embed  REQUIRED_KEYS = ( | 
