diff options
Diffstat (limited to 'pydis_site')
21 files changed, 393 insertions, 28 deletions
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 059f52eb..0333fefc 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -12,6 +12,7 @@ from .models import (      MessageDeletionContext,      Nomination,      OffTopicChannelName, +    OffensiveMessage,      Role,      Tag,      User @@ -60,6 +61,7 @@ admin.site.register(Infraction)  admin.site.register(LogEntry, LogEntryAdmin)  admin.site.register(MessageDeletionContext)  admin.site.register(Nomination) +admin.site.register(OffensiveMessage)  admin.site.register(OffTopicChannelName)  admin.site.register(Role)  admin.site.register(Tag) diff --git a/pydis_site/apps/api/migrations/0049_deletedmessage_attachments.py b/pydis_site/apps/api/migrations/0049_deletedmessage_attachments.py new file mode 100644 index 00000000..31ac239a --- /dev/null +++ b/pydis_site/apps/api/migrations/0049_deletedmessage_attachments.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.6 on 2019-10-28 17:12 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0049_offensivemessage'), +    ] + +    operations = [ +        migrations.AddField( +            model_name='deletedmessage', +            name='attachments', +            field=django.contrib.postgres.fields.ArrayField(base_field=models.URLField(max_length=512), default=[], blank=True, help_text='Attachments attached to this message.', size=None), +            preserve_default=False, +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0049_offensivemessage.py b/pydis_site/apps/api/migrations/0049_offensivemessage.py new file mode 100644 index 00000000..fe4a1961 --- /dev/null +++ b/pydis_site/apps/api/migrations/0049_offensivemessage.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.6 on 2019-11-07 18:08 + +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.offensive_message +import pydis_site.apps.api.models.utils + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0048_add_infractions_unique_constraints_active'), +    ] + +    operations = [ +        migrations.CreateModel( +            name='OffensiveMessage', +            fields=[ +                ('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 the message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), +                ('delete_date', models.DateTimeField(help_text='The date on which the message will be auto-deleted.', validators=[pydis_site.apps.api.models.bot.offensive_message.future_date_validator])), +            ], +            bases=(pydis_site.apps.api.models.utils.ModelReprMixin, models.Model), +        ), +    ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index a4656bc3..450d18cd 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -7,6 +7,7 @@ from .bot import (      Message,      MessageDeletionContext,      Nomination, +    OffensiveMessage,      OffTopicChannelName,      Reminder,      Role, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 46219ea2..8ae47746 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -7,6 +7,7 @@ from .message import Message  from .message_deletion_context import MessageDeletionContext  from .nomination import Nomination  from .off_topic_channel_name import OffTopicChannelName +from .offensive_message import OffensiveMessage  from .reminder import Reminder  from .role import Role  from .tag import Tag diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 31316a01..8b18fc9f 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -51,6 +51,13 @@ class Message(ModelReprMixin, models.Model):          ),          help_text="Embeds attached to this message."      ) +    attachments = pgfields.ArrayField( +        models.URLField( +            max_length=512 +        ), +        blank=True, +        help_text="Attachments attached to this message." +    )      @property      def timestamp(self) -> datetime: diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py new file mode 100644 index 00000000..b466d9c2 --- /dev/null +++ b/pydis_site/apps/api/models/bot/offensive_message.py @@ -0,0 +1,48 @@ +import datetime + +from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +def future_date_validator(date: datetime.date) -> None: +    """Raise ValidationError if the date isn't a future date.""" +    if date < datetime.datetime.now(datetime.timezone.utc): +        raise ValidationError("Date must be a future date") + + +class OffensiveMessage(ModelReprMixin, models.Model): +    """A message that triggered a filter and that will be deleted one week after it was sent.""" + +    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." +            ), +        ) +    ) +    channel_id = models.BigIntegerField( +        help_text=( +            "The channel ID that the message was " +            "sent in, taken from Discord." +        ), +        validators=( +            MinValueValidator( +                limit_value=0, +                message="Channel IDs cannot be negative." +            ), +        ) +    ) +    delete_date = models.DateTimeField( +        help_text="The date on which the message will be auto-deleted.", +        validators=(future_date_validator,) +    ) + +    def __str__(self): +        """Return some info on this message, for display purposes only.""" +        return f"Message {self.id}, will be deleted at {self.delete_date}" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 4e7cd863..0d1a4684 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -8,6 +8,7 @@ from .models import (      DocumentationLink, Infraction,      LogEntry, MessageDeletionContext,      Nomination, OffTopicChannelName, +    OffensiveMessage,      Reminder, Role,      Tag, User  ) @@ -49,7 +50,8 @@ class DeletedMessageSerializer(ModelSerializer):          fields = (              'id', 'author',              'channel_id', 'content', -            'embeds', 'deletion_context' +            'embeds', 'deletion_context', +            'attachments'          ) @@ -247,3 +249,13 @@ class NominationSerializer(ModelSerializer):          fields = (              'id', 'active', 'actor', 'reason', 'user',              'inserted_at', 'end_reason', 'ended_at') + + +class OffensiveMessageSerializer(ModelSerializer): +    """A class providing (de-)serialization of `OffensiveMessage` instances.""" + +    class Meta: +        """Metadata defined for the Django REST Framework.""" + +        model = OffensiveMessage +        fields = ('id', 'channel_id', 'delete_date') diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index d1e9f2f5..b3a8197b 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -25,14 +25,16 @@ class DeletedMessagesWithoutActorTests(APISubdomainTestCase):                      'id': 55,                      'channel_id': 5555,                      'content': "Terror Billy is a meanie", -                    'embeds': [] +                    'embeds': [], +                    'attachments': []                  },                  {                      'author': cls.author.id,                      'id': 56,                      'channel_id': 5555,                      'content': "If you purge this, you're evil", -                    'embeds': [] +                    'embeds': [], +                    'attachments': []                  }              ]          } @@ -64,7 +66,8 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase):                      'id': 12903,                      'channel_id': 1824,                      'content': "I hate trailing commas", -                    'embeds': [] +                    'embeds': [], +                    'attachments': []                  },              ]          } diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index b4a766d0..a97d3251 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -12,6 +12,7 @@ from ..models import (      ModelReprMixin,      Nomination,      OffTopicChannelName, +    OffensiveMessage,      Reminder,      Role,      Tag, @@ -69,6 +70,11 @@ class StringDunderMethodTests(SimpleTestCase):              DocumentationLink(                  'test', 'http://example.com', 'http://example.com'              ), +            OffensiveMessage( +                id=602951077675139072, +                channel_id=291284109232308226, +                delete_date=dt(3000, 1, 1) +            ),              OffTopicChannelName(name='bob-the-builders-playground'),              Role(                  id=5, name='test role', diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py new file mode 100644 index 00000000..d5896714 --- /dev/null +++ b/pydis_site/apps/api/tests/test_offensive_message.py @@ -0,0 +1,155 @@ +import datetime + +from django_hosts.resolvers import reverse + +from .base import APISubdomainTestCase +from ..models import OffensiveMessage + + +class CreationTests(APISubdomainTestCase): +    def test_accept_valid_data(self): +        url = reverse('bot:offensivemessage-list', host='api') +        delete_at = datetime.datetime.now() + datetime.timedelta(days=1) +        data = { +            'id': '602951077675139072', +            'channel_id': '291284109232308226', +            'delete_date': delete_at.isoformat()[:-1] +        } + +        aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc) + +        response = self.client.post(url, data=data) +        self.assertEqual(response.status_code, 201) + +        offensive_message = OffensiveMessage.objects.get(id=response.json()['id']) +        self.assertAlmostEqual( +            aware_delete_at, +            offensive_message.delete_date, +            delta=datetime.timedelta(seconds=1) +        ) +        self.assertEqual(data['id'], str(offensive_message.id)) +        self.assertEqual(data['channel_id'], str(offensive_message.channel_id)) + +    def test_returns_400_on_non_future_date(self): +        url = reverse('bot:offensivemessage-list', host='api') +        delete_at = datetime.datetime.now() - datetime.timedelta(days=1) +        data = { +            'id': '602951077675139072', +            'channel_id': '291284109232308226', +            'delete_date': delete_at.isoformat()[:-1] +        } +        response = self.client.post(url, data=data) +        self.assertEqual(response.status_code, 400) +        self.assertEqual(response.json(), { +            'delete_date': ['Date must be a future date'] +        }) + +    def test_returns_400_on_negative_id_or_channel_id(self): +        url = reverse('bot:offensivemessage-list', host='api') +        delete_at = datetime.datetime.now() + datetime.timedelta(days=1) +        data = { +            'id': '602951077675139072', +            'channel_id': '291284109232308226', +            'delete_date': delete_at.isoformat()[:-1] +        } +        cases = ( +            ('id', '-602951077675139072'), +            ('channel_id', '-291284109232308226') +        ) + +        for field, invalid_value in cases: +            with self.subTest(fied=field, invalid_value=invalid_value): +                test_data = data.copy() +                test_data.update({field: invalid_value}) + +                response = self.client.post(url, test_data) +                self.assertEqual(response.status_code, 400) +                self.assertEqual(response.json(), { +                    field: ['Ensure this value is greater than or equal to 0.'] +                }) + + +class ListTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls):  # noqa +        delete_at = datetime.datetime.now() + datetime.timedelta(days=1) +        aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc) + +        cls.messages = [ +            { +                'id': 602951077675139072, +                'channel_id': 91284109232308226, +            }, +            { +                'id': 645298201494159401, +                'channel_id': 592000283102674944 +            } +        ] + +        cls.of1 = OffensiveMessage.objects.create( +            **cls.messages[0], +            delete_date=aware_delete_at.isoformat() +        ) +        cls.of2 = OffensiveMessage.objects.create( +            **cls.messages[1], +            delete_date=aware_delete_at.isoformat() +        ) + +        # Expected API answer : +        cls.messages[0]['delete_date'] = delete_at.isoformat() + 'Z' +        cls.messages[1]['delete_date'] = delete_at.isoformat() + 'Z' + +    def test_get_data(self): +        url = reverse('bot:offensivemessage-list', host='api') + +        response = self.client.get(url) +        self.assertEqual(response.status_code, 200) + +        self.assertEqual(response.json(), self.messages) + + +class DeletionTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls):  # noqa +        delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) + +        cls.valid_offensive_message = OffensiveMessage.objects.create( +            id=602951077675139072, +            channel_id=291284109232308226, +            delete_date=delete_at.isoformat() +        ) + +    def test_delete_data(self): +        url = reverse( +            'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) +        ) + +        response = self.client.delete(url) +        self.assertEqual(response.status_code, 204) + +        self.assertFalse( +            OffensiveMessage.objects.filter(id=self.valid_offensive_message.id).exists() +        ) + + +class NotAllowedMethodsTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls):  # noqa +        delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) + +        cls.valid_offensive_message = OffensiveMessage.objects.create( +            id=602951077675139072, +            channel_id=291284109232308226, +            delete_date=delete_at.isoformat() +        ) + +    def test_returns_405_for_patch_and_put_requests(self): +        url = reverse( +            'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) +        ) +        not_allowed_methods = (self.client.patch, self.client.put) + +        for method in not_allowed_methods: +            with self.subTest(method=method): +                response = method(url, {}) +                self.assertEqual(response.status_code, 405) diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py index 4222f0c0..241af08c 100644 --- a/pydis_site/apps/api/tests/test_validators.py +++ b/pydis_site/apps/api/tests/test_validators.py @@ -1,7 +1,10 @@ +from datetime import datetime, timezone +  from django.core.exceptions import ValidationError  from django.test import TestCase  from ..models.bot.bot_setting import validate_bot_setting_name +from ..models.bot.offensive_message import future_date_validator  from ..models.bot.tag import validate_tag_embed @@ -245,3 +248,12 @@ class TagEmbedValidatorTests(TestCase):                  'name': "Bob"              }          }) + + +class OffensiveMessageValidatorsTests(TestCase): +    def test_accepts_future_date(self): +        future_date_validator(datetime(3000, 1, 1, tzinfo=timezone.utc)) + +    def test_rejects_non_future_date(self): +        with self.assertRaises(ValidationError): +            future_date_validator(datetime(1000, 1, 1, tzinfo=timezone.utc)) diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index ac6704c8..4a0281b4 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -6,7 +6,8 @@ from .viewsets import (      BotSettingViewSet, DeletedMessageViewSet,      DocumentationLinkViewSet, InfractionViewSet,      LogEntryViewSet, NominationViewSet, -    OffTopicChannelNameViewSet, ReminderViewSet, +    OffTopicChannelNameViewSet, +    OffensiveMessageViewSet, ReminderViewSet,      RoleViewSet, TagViewSet, UserViewSet  ) @@ -34,6 +35,10 @@ bot_router.register(      NominationViewSet  )  bot_router.register( +    'offensive-messages', +    OffensiveMessageViewSet +) +bot_router.register(      'off-topic-channel-names',      OffTopicChannelNameViewSet,      base_name='offtopicchannelname' diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index f9a186d9..3cf9f641 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -5,6 +5,7 @@ from .bot import (      DocumentationLinkViewSet,      InfractionViewSet,      NominationViewSet, +    OffensiveMessageViewSet,      OffTopicChannelNameViewSet,      ReminderViewSet,      RoleViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index f1851e32..b3e0fa4d 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -5,6 +5,7 @@ from .documentation_link import DocumentationLinkViewSet  from .infraction import InfractionViewSet  from .nomination import NominationViewSet  from .off_topic_channel_name import OffTopicChannelNameViewSet +from .offensive_message import OffensiveMessageViewSet  from .reminder import ReminderViewSet  from .role import RoleViewSet  from .tag import TagViewSet diff --git a/pydis_site/apps/api/viewsets/bot/offensive_message.py b/pydis_site/apps/api/viewsets/bot/offensive_message.py new file mode 100644 index 00000000..54cb3a38 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/offensive_message.py @@ -0,0 +1,61 @@ +from rest_framework.mixins import ( +    CreateModelMixin, +    DestroyModelMixin, +    ListModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.offensive_message import OffensiveMessage +from pydis_site.apps.api.serializers import OffensiveMessageSerializer + + +class OffensiveMessageViewSet( +    CreateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet +): +    """ +    View providing CRUD access to offensive messages. + +    ## Routes +    ### GET /bot/offensive-messages +    Returns all offensive messages in the database. + +    #### Response format +    >>> [ +    ...     { +    ...         'id': '631953598091100200', +    ...         'channel_id': '291284109232308226', +    ...         'delete_date': '2019-11-01T21:51:15.545000Z' +    ...     }, +    ...     ... +    ... ] + +    #### Status codes +    - 200: returned on success + +    ### POST /bot/offensive-messages +    Create a new offensive message object. + +    #### Request body +    >>> { +    ...     'id': int, +    ...     'channel_id': int, +    ...     'delete_date': datetime.datetime  # ISO-8601-formatted date +    ... } + +    #### Status codes +    - 201: returned on success +    - 400: if the body format is invalid + +    ### DELETE /bot/offensive-messages/<id:int> +    Delete the offensive message object with the given `id`. + +    #### Status codes +    - 204: returned on success +    - 404: if a offensive message object with the given `id` does not exist + +    ## Authentication +    Requires an API token. +    """ + +    serializer_class = OffensiveMessageSerializer +    queryset = OffensiveMessage.objects.all() diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index 32cb6bbf..1415c558 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -37,6 +37,7 @@ class TestLogsView(TestCase):              channel_id=1984,              content='<em>I think my tape has run out...</em>',              embeds=[], +            attachments=[],              deletion_context=cls.deletion_context,          ) @@ -101,6 +102,7 @@ class TestLogsView(TestCase):              channel_id=1984,              content='Does that mean this thing will halt?',              embeds=[cls.embed_one, cls.embed_two], +            attachments=['https://http.cat/100', 'https://http.cat/402'],              deletion_context=cls.deletion_context,          ) @@ -149,6 +151,21 @@ class TestLogsView(TestCase):          self.assertInHTML(embed_colour_needle.format(colour=embed_one_colour), html_response)          self.assertInHTML(embed_colour_needle.format(colour=embed_two_colour), html_response) +    def test_if_both_attachments_are_included_html_response(self): +        url = reverse('logs', host="staff", args=(self.deletion_context.id,)) +        response = self.client.get(url) + +        html_response = response.content.decode() +        attachment_needle = '<img alt="Attachment" class="discord-attachment" src="{url}">' +        self.assertInHTML( +            attachment_needle.format(url=self.deleted_message_two.attachments[0]), +            html_response +        ) +        self.assertInHTML( +            attachment_needle.format(url=self.deleted_message_two.attachments[1]), +            html_response +        ) +      def test_if_html_in_content_is_properly_escaped(self):          url = reverse('logs', host="staff", args=(self.deletion_context.id,))          response = self.client.get(url) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 66376c4e..65ef2463 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -360,24 +360,7 @@ WIKI_MESSAGE_TAG_CSS_CLASS = {      messages.WARNING: "is-warning",  } -WIKI_MARKDOWN_HTML_STYLES = [ -    'max-width', -    'min-width', -    'margin', -    'padding', -    'width', -    'height', -] - -WIKI_MARKDOWN_HTML_ATTRIBUTES = { -    'img': ['class', 'id', 'src', 'alt', 'width', 'height'], -    'section': ['class', 'id'], -    'article': ['class', 'id'], -} - -WIKI_MARKDOWN_HTML_WHITELIST = [ -    'article', 'section', 'button' -] +WIKI_MARKDOWN_SANITIZE_HTML = False  # Wiki permissions diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index dfcc6715..3b150767 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -37,11 +37,11 @@            </p>          </div> -        {# Intro video #} +        {# Code Jam banner #}          <div class="column is-half-desktop video-container"> -          <iframe src="https://www.youtube.com/embed/DIBXg8Qh7bA" frameborder="0" -                  allow="accelerometer; encrypted-media; gyroscope; picture-in-picture" -                  allowfullscreen></iframe> +          <a href="https://pythondiscord.com/pages/code-jams/code-jam-6/"> +            <img src="https://raw.githubusercontent.com/python-discord/branding/master/logos/logo_discord_banner/code%20jam%206%20-%20website%20banner.png"/> +          </a>          </div>        </div> diff --git a/pydis_site/templates/staff/logs.html b/pydis_site/templates/staff/logs.html index 9c8ed7d3..a0bfa2a7 100644 --- a/pydis_site/templates/staff/logs.html +++ b/pydis_site/templates/staff/logs.html @@ -24,6 +24,11 @@              <div class="discord-message-content">                  {{ message.content|linebreaks }}              </div> +            <div class="discord-message-attachments"> +                {% for attachment in message.attachments %} +                    <img alt="Attachment" class="discord-attachment" src="{{ attachment }}"> +                {% endfor %} +            </div>              {% for embed in message.embeds %}                  <div class="discord-embed is-size-7">                      <div class="discord-embed-color" style="background-color: {% if embed.color %}{{ embed.color | hex_colour }}{% else %}#cacbce{% endif %}"></div> diff --git a/pydis_site/utils/account.py b/pydis_site/utils/account.py index e64919de..2d699c88 100644 --- a/pydis_site/utils/account.py +++ b/pydis_site/utils/account.py @@ -73,7 +73,7 @@ class SocialAccountAdapter(DefaultSocialAccountAdapter):          """          if social_login.account.provider == "discord":              discriminator = social_login.account.extra_data["discriminator"] -            data["username"] = f"{data['username']}#{discriminator}" +            data["username"] = f"{data['username']}#{discriminator:0>4}"              data["name"] = data["username"]          return super().populate_user(request, social_login, data)  |