From ab8b798547e82ca79882ba28b1920077c803425f Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Fri, 5 Apr 2019 18:24:32 +0100 Subject: pysite -> pydis_site --- pydis_site/apps/api/validators.py | 164 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 pydis_site/apps/api/validators.py (limited to 'pydis_site/apps/api/validators.py') diff --git a/pydis_site/apps/api/validators.py b/pydis_site/apps/api/validators.py new file mode 100644 index 00000000..ac2fb739 --- /dev/null +++ b/pydis_site/apps/api/validators.py @@ -0,0 +1,164 @@ +from collections.abc import Mapping + +from django.core.exceptions import ValidationError +from django.core.validators import MaxLengthValidator, MinLengthValidator + + +def validate_tag_embed_fields(fields): + field_validators = { + 'name': (MaxLengthValidator(limit_value=256),), + 'value': (MaxLengthValidator(limit_value=1024),) + } + + for field in fields: + if not isinstance(field, Mapping): + raise ValidationError("Embed fields must be a mapping.") + + for field_name, value in field.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed field field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed_footer(footer): + field_validators = { + 'text': ( + MinLengthValidator( + limit_value=1, + message="Footer text must not be empty." + ), + MaxLengthValidator(limit_value=2048) + ), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(footer, Mapping): + raise ValidationError("Embed footer must be a mapping.") + + for field_name, value in footer.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed footer field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed_author(author): + field_validators = { + 'name': ( + MinLengthValidator( + limit_value=1, + message="Embed author name must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'url': (), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(author, Mapping): + raise ValidationError("Embed author must be a mapping.") + + for field_name, value in author.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed author field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed(embed): + """ + Validate a JSON document containing an embed as possible to send + on Discord. This attempts to rebuild the validation used by Discord + as well as possible by checking for various embed limits so we can + ensure that any embed we store here will also be accepted as a + valid embed by the Discord API. + + Using this directly is possible, although not intended - you usually + stick this onto the `validators` keyword argument of model fields. + + Example: + + >>> from django.contrib.postgres import fields as pgfields + >>> from django.db import models + >>> from pydis_site.apps.api import validate_tag_embed + >>> class MyMessage(models.Model): + ... embed = pgfields.JSONField( + ... validators=( + ... validate_tag_embed, + ... ) + ... ) + ... # ... + ... + + Args: + embed (Dict[str, Union[str, List[dict], dict]]): + A dictionary describing the contents of this embed. + See the official documentation for a full reference + of accepted keys by this dictionary: + https://discordapp.com/developers/docs/resources/channel#embed-object + + Raises: + ValidationError: + In case the given embed is deemed invalid, a `ValidationError` + is raised which in turn will allow Django to display errors + as appropriate. + """ + + all_keys = { + 'title', 'type', 'description', 'url', 'timestamp', + 'color', 'footer', 'image', 'thumbnail', 'video', + 'provider', 'author', 'fields' + } + one_required_of = {'description', 'fields', 'image', 'title', 'video'} + field_validators = { + 'title': ( + MinLengthValidator( + limit_value=1, + message="Embed title must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'description': (MaxLengthValidator(limit_value=2048),), + 'fields': ( + MaxLengthValidator(limit_value=25), + validate_tag_embed_fields + ), + 'footer': (validate_tag_embed_footer,), + 'author': (validate_tag_embed_author,) + } + + if not embed: + raise ValidationError("Tag embed must not be empty.") + + elif not isinstance(embed, Mapping): + raise ValidationError("Tag embed must be a mapping.") + + elif not any(field in embed for field in one_required_of): + raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") + + for required_key in one_required_of: + if required_key in embed and not embed[required_key]: + raise ValidationError(f"Key {required_key!r} must not be empty.") + + for field_name, value in embed.items(): + if field_name not in all_keys: + raise ValidationError(f"Unknown field name: {field_name!r}") + + if field_name in field_validators: + for validator in field_validators[field_name]: + validator(value) + + +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.") -- cgit v1.2.3 From d06fd493b865ae2936709cc0875bbb9d56e58c62 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Sun, 7 Apr 2019 18:35:37 +0100 Subject: Address review by @jchristgit --- pydis_site/apps/api/tests/base.py | 6 +++--- pydis_site/apps/api/validators.py | 2 +- pydis_site/apps/api/viewsets.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'pydis_site/apps/api/validators.py') diff --git a/pydis_site/apps/api/tests/base.py b/pydis_site/apps/api/tests/base.py index 0290fa69..8f8ace56 100644 --- a/pydis_site/apps/api/tests/base.py +++ b/pydis_site/apps/api/tests/base.py @@ -28,7 +28,7 @@ class APISubdomainTestCase(APITestCase): If you don't want to force authentication (for example, to test a route's response for an unauthenticated user), un-force authentication by using the following: - >>> from pydis_site.apps.api import APISubdomainTestCase + >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase >>> class UnauthedUserTestCase(APISubdomainTestCase): ... def setUp(self): ... super().setUp() @@ -46,7 +46,7 @@ class APISubdomainTestCase(APITestCase): ## Example Using this in a test case is rather straightforward: - >>> from pydis_site.apps.api import APISubdomainTestCase + >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase >>> class MyAPITestCase(APISubdomainTestCase): ... def test_that_it_works(self): ... response = self.client.get('/my-endpoint') @@ -55,7 +55,7 @@ class APISubdomainTestCase(APITestCase): To reverse URLs of the API host, you need to use `django_hosts`: >>> from django_hosts.resolvers import reverse - >>> from pydis_site.apps.api import APISubdomainTestCase + >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase >>> class MyReversedTestCase(APISubdomainTestCase): ... def test_my_endpoint(self): ... url = reverse('user-detail', host='api') diff --git a/pydis_site/apps/api/validators.py b/pydis_site/apps/api/validators.py index ac2fb739..69a8d1ef 100644 --- a/pydis_site/apps/api/validators.py +++ b/pydis_site/apps/api/validators.py @@ -86,7 +86,7 @@ def validate_tag_embed(embed): >>> from django.contrib.postgres import fields as pgfields >>> from django.db import models - >>> from pydis_site.apps.api import validate_tag_embed + >>> from pydis_site.apps.api.validators import validate_tag_embed >>> class MyMessage(models.Model): ... embed = pgfields.JSONField( ... validators=( diff --git a/pydis_site/apps/api/viewsets.py b/pydis_site/apps/api/viewsets.py index 17024fe8..949ffaaa 100644 --- a/pydis_site/apps/api/viewsets.py +++ b/pydis_site/apps/api/viewsets.py @@ -383,7 +383,7 @@ class ReminderViewSet(CreateModelMixin, ListModelMixin, DestroyModelMixin, Gener ... 'active': True, ... 'author': 1020103901030, ... 'content': "Make dinner", - ... 'expiration': '5018-11-20T15:52:00Z' + ... 'expiration': '5018-11-20T15:52:00Z', ... 'id': 11 ... }, ... ... -- cgit v1.2.3