1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
|
from collections.abc import Mapping
from operator import itemgetter
from typing import Any, Dict
from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, MinLengthValidator
def is_bool_validator(value: Any) -> None:
"""Validates if a given value is of type bool."""
if not isinstance(value, bool):
raise ValidationError(f"This field must be of type bool, not {type(value)}.")
def validate_embed_fields(fields: dict) -> None:
"""Raises a ValidationError if any of the given embed fields is invalid."""
field_validators = {
'name': (MaxLengthValidator(limit_value=256),),
'value': (MaxLengthValidator(limit_value=1024),),
'inline': (is_bool_validator,),
}
required_fields = ('name', 'value')
for field in fields:
if not isinstance(field, Mapping):
raise ValidationError("Embed fields must be a mapping.")
if not all(required_field in field for required_field in required_fields):
raise ValidationError(
f"Embed fields must contain the following fields: {', '.join(required_fields)}."
)
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_embed_footer(footer: Dict[str, str]) -> None:
"""Raises a ValidationError if the given footer is invalid."""
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_embed_author(author: Any) -> None:
"""Raises a ValidationError if the given author is invalid."""
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_embed(embed: Any) -> None:
"""
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.models.utils import validate_embed
>>> class MyMessage(models.Model):
... embed = pgfields.JSONField(
... validators=(
... validate_embed,
... )
... )
... # ...
...
Args:
embed (Any):
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_embed_fields
),
'footer': (validate_embed_footer,),
'author': (validate_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)
from django.db import models
class ModelReprMixin:
"""Mixin providing a `__repr__()` to display model class name and initialisation parameters."""
def __repr__(self):
"""Returns the current model class name and initialisation parameters."""
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 ModelTimestampMixin(models.Model):
"""Mixin providing created_at and updated_at fields."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
"""Metaconfig for the mixin."""
abstract = True
|