Completed
Push — main ( 3a3ea0...93fa25 )
by Yohann
23s queued 12s
created

EmbedField.__post_init__()   A

Complexity

Conditions 3

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 3
nop 1
1
# Copyright Pincer 2021-Present
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
# Full MIT License can be found in `LICENSE` at the project root.
3
4
from __future__ import annotations
5
6
from dataclasses import dataclass, field
7
from datetime import datetime
8
from re import match
9
from typing import Any, Callable, Dict, Iterable, Union, Optional, TYPE_CHECKING
10
11
from ...exceptions import InvalidUrlError, EmbedFieldError
12
from ...utils.api_object import APIObject
13
from ...utils.types import MISSING
14
15
if TYPE_CHECKING:
16
    from ...utils import APINullable
17
18
19
def _field_size(_field: str) -> int:
20
    """
21
    The Discord API removes white space
22
        when counting the length of a field.
23
24
    :param _field:
25
        The field.
26
27
    :return:
28
        Length of the string without white space.
29
    """
30
    return 0 if _field == MISSING else len(_field.strip())
31
32
33
def _is_valid_url(url: str) -> bool:
34
    """
35
    Checks whether the url is a proper and valid url.
36
    (matches for http and attachment protocol.
37
38
    :param url:
39
        The url which must be checked.
40
41
    :return:
42
        Whether the provided url is valid.
43
    """
44
    stmt = (
45
        r"(http[s]|attachment)"
46
        r"?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|"
47
        r"(?:%[0-9a-fA-F][0-9a-fA-F]))+"
48
    )
49
50
    return bool(match(stmt, url))
51
52
53
def _check_if_valid_url(url: str):
54
    """
55
    Checks if the provided url is valid.
56
57
    :raises InvalidUrlError:
58
        if the url didn't match the url regex.
59
        (which means that it was malformed or didn't match the http/attachment
60
        protocol).
61
    """
62
    if not _is_valid_url(url):
63
        raise InvalidUrlError(
64
            "Url was malformed or wasn't of protocol http(s)/attachment."
65
        )
66
67
68
@dataclass
69
class EmbedAuthor:
70
    """
71
    Representation of the Embed Author class
72
73
    :param name:
74
        Name of the author
75
76
    :param url:
77
        Url of the author
78
79
    :param icon_url:
80
        Url of the author icon
81
82
    :param proxy_icon_url:
83
        A proxied url of the author icon
84
    """
85
    icon_url: APINullable[str] = MISSING
0 ignored issues
show
introduced by
The variable APINullable does not seem to be defined in case TYPE_CHECKING on line 15 is False. Are you sure this can never be the case?
Loading history...
86
    name: APINullable[str] = MISSING
87
    proxy_icon_url: APINullable[str] = MISSING
88
    url: APINullable[str] = MISSING
89
90
    def __post_init__(self):
91
        """
92
        :raises EmbedFieldError:
93
            Name is longer than 256 characters.
94
95
        :raises InvalidUrlError:
96
            if the url didn't match the url regex.
97
            (which means that it was malformed or didn't match the
98
            http/attachment protocol).
99
        """
100
        if _field_size(self.name) > 256:
101
            raise EmbedFieldError.from_desc("Author name", 256, len(self.name))
102
103
        _check_if_valid_url(self.url)
104
105
106
@dataclass
107
class EmbedImage:
108
    """
109
    Representation of the Embed Image class
110
111
    :param url:
112
        Source url of the image
113
114
    :param proxy_url:
115
        A proxied url of the image
116
117
    :param height:
118
        Height of the image
119
120
    :param width:
121
        Width of the image
122
    """
123
124
    height: APINullable[int] = MISSING
0 ignored issues
show
introduced by
The variable APINullable does not seem to be defined in case TYPE_CHECKING on line 15 is False. Are you sure this can never be the case?
Loading history...
125
    proxy_url: APINullable[str] = MISSING
126
    url: APINullable[str] = MISSING
127
    width: APINullable[int] = MISSING
128
129
    def __post_init__(self):
130
        """
131
        :raises InvalidUrlError:
132
            if the url didn't match the url regex.
133
            (which means that it was malformed or didn't match the
134
            http/attachment protocol).
135
        """
136
        _check_if_valid_url(self.url)
137
138
139
@dataclass
140
class EmbedProvider:
141
    """
142
    Representation of the Provider class
143
144
    :param name:
145
        Name of the provider
146
147
    :param url:
148
        Url of the provider
149
    """
150
    name: APINullable[str] = MISSING
0 ignored issues
show
introduced by
The variable APINullable does not seem to be defined in case TYPE_CHECKING on line 15 is False. Are you sure this can never be the case?
Loading history...
151
    url: APINullable[str] = MISSING
152
153
154
@dataclass
155
class EmbedThumbnail:
156
    """
157
    Representation of the Embed Thumbnail class
158
159
    :param url:
160
        Source url of the thumbnail
161
162
    :param proxy_url:
163
        A proxied url of the thumbnail
164
165
    :param height:
166
        Height of the thumbnail
167
168
    :param width:
169
        Width of the thumbnail
170
171
    """
172
173
    height: APINullable[int] = MISSING
0 ignored issues
show
introduced by
The variable APINullable does not seem to be defined in case TYPE_CHECKING on line 15 is False. Are you sure this can never be the case?
Loading history...
174
    proxy_url: APINullable[str] = MISSING
175
    url: APINullable[str] = MISSING
176
    width: APINullable[int] = MISSING
177
178
    def __post_init__(self):
179
        """
180
        :raises InvalidUrlError:
181
            if the url didn't match the url regex.
182
            (which means that it was malformed or didn't match the
183
            http/attachment protocol).
184
        """
185
        _check_if_valid_url(self.url)
186
187
188
@dataclass
189
class EmbedVideo:
190
    """
191
    Representation of the Embed Video class
192
193
    :param url:
194
        Source url of the video
195
196
    :param proxy_url:
197
        A proxied url of the video
198
199
    :param height:
200
        Height of the video
201
202
    :param width:
203
        Width of the video
204
    """
205
    height: APINullable[int] = MISSING
0 ignored issues
show
introduced by
The variable APINullable does not seem to be defined in case TYPE_CHECKING on line 15 is False. Are you sure this can never be the case?
Loading history...
206
    url: APINullable[str] = MISSING
207
    proxy_url: APINullable[str] = MISSING
208
    width: APINullable[int] = MISSING
209
210
211
@dataclass
212
class EmbedFooter:
213
    """
214
    Representation of the Embed Footer class
215
216
    :param text:
217
        Footer text
218
219
    :param icon_url:
220
        Url of the footer icon
221
222
    :param proxy_icon_url:
223
        A proxied url of the footer icon
224
225
    :raises EmbedFieldError:
226
        Text is longer than 2048 characters
227
    """
228
229
    text: str
230
231
    icon_url: APINullable[str] = MISSING
0 ignored issues
show
introduced by
The variable APINullable does not seem to be defined in case TYPE_CHECKING on line 15 is False. Are you sure this can never be the case?
Loading history...
232
    proxy_icon_url: APINullable[str] = MISSING
233
234
    def __post_init__(self):
235
        if _field_size(self.text) > 2048:
236
            raise EmbedFieldError.from_desc(
237
                "Footer text", 2048, len(self.text)
238
            )
239
240
241
@dataclass
242
class EmbedField:
243
    """
244
    Representation of the Embed Field class
245
246
    :param name:
247
        The name of the field
248
249
    :param value:
250
        The text in the field
251
252
    :param inline:
253
        Whether or not this field should display inline
254
255
    :raises EmbedFieldError:
256
        Name is longer than 256 characters
257
258
    :raises EmbedFieldError:
259
        Description is longer than 1024 characters
260
    """
261
262
    name: str
263
    value: str
264
265
    inline: APINullable[bool] = MISSING
0 ignored issues
show
introduced by
The variable APINullable does not seem to be defined in case TYPE_CHECKING on line 15 is False. Are you sure this can never be the case?
Loading history...
266
267
    def __post_init__(self):
268
        if _field_size(self.name) > 256:
269
            raise EmbedFieldError.from_desc(
270
                "Field name", 256, len(self.name)
271
            )
272
273
        if _field_size(self.value) > 1024:
274
            raise EmbedFieldError.from_desc(
275
                "Field value", 1024, len(self.value)
276
            )
277
278
279
# TODO: Handle Bad Request if embed that is too big is sent
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
280
# https://discord.com/developers/docs/resources/channel#embed-limits
281
# Currently ignored since I don't think it would make sense to put
282
# This with the Embed class
283
@dataclass
0 ignored issues
show
best-practice introduced by
Too many instance attributes (13/7)
Loading history...
284
class Embed(APIObject):
285
    """
286
    Representation of the discord Embed class
287
288
    :param title:
289
        Embed title.
290
291
    :param description:
292
        Embed description.
293
294
    :param color:
295
        Embed color code.
296
297
    :param fields:
298
        Fields information.
299
300
    :param footer:
301
        Footer information.
302
303
    :param image:
304
        Image information.
305
306
    :param provider:
307
        Provider information.
308
309
    :param thumbnail:
310
        Thumbnail information.
311
312
    :param timestamp:
313
        Timestamp of embed content in ISO format.
314
315
    :property author:
316
        Author information.
317
318
    :param url:
319
        Embed url.
320
321
    :param video:
322
        Video information.
323
324
    :param type:
325
        type of message
326
    """
327
328
    title: APINullable[str] = MISSING
0 ignored issues
show
introduced by
The variable APINullable does not seem to be defined in case TYPE_CHECKING on line 15 is False. Are you sure this can never be the case?
Loading history...
329
    description: APINullable[str] = MISSING
330
    color: APINullable[int] = MISSING
331
    fields: list[EmbedField] = field(default_factory=list)
0 ignored issues
show
introduced by
Value 'list' is unsubscriptable
Loading history...
332
    footer: APINullable[EmbedFooter] = MISSING
333
    image: APINullable[EmbedImage] = MISSING
334
    provider: APINullable[EmbedProvider] = MISSING
335
    thumbnail: APINullable[EmbedThumbnail] = MISSING
336
    timestamp: APINullable[str] = MISSING
337
    author: APINullable[EmbedAuthor] = MISSING
338
    url: APINullable[str] = MISSING
339
    video: APINullable[EmbedVideo] = MISSING
340
    type: APINullable[int] = MISSING
341
342
    def __post_init__(self):
343
        """
344
        :raises EmbedFieldError:
345
            Embed title is longer than 256 characters.
346
        :raises EmbedFieldError:
347
            Embed description is longer than 4096 characters.
348
        :raises EmbedFieldError:
349
            Embed has more than 25 fields.
350
        """
351
        if _field_size(self.title) > 256:
352
            raise EmbedFieldError.from_desc(
353
                "Embed title", 256, len(self.title)
354
            )
355
356
        if _field_size(self.description) > 4096:
357
            raise EmbedFieldError.from_desc(
358
                "Embed description", 4096, len(self.description)
359
            )
360
361
        if len(self.fields) > 25:
362
            raise EmbedFieldError.from_desc(
363
                "Embed field", 25, len(self.fields)
364
            )
365
366
    def set_timestamp(self, time: datetime) -> Embed:
367
        """
368
        Discord uses iso format for time stamps.
369
        This function will set the time to that format.
370
371
        :param time:
372
            A datetime object.
373
374
        :return: self
375
        """
376
        self.timestamp = time.isoformat()
377
378
        return self
379
380
    def set_author(
381
            self,
382
            icon_url: APINullable[str] = MISSING,
383
            name: APINullable[str] = MISSING,
384
            proxy_icon_url: APINullable[str] = MISSING,
385
            url: APINullable[str] = MISSING
386
    ) -> Embed:
387
        """
388
        Set the author message for the embed. This is the top
389
        field of the embed.
390
391
        :param icon_url:
392
            The icon which will be next to the author name.
393
394
        :param name:
395
            The name for the author (so the message).
396
397
        :param proxy_icon_url:
398
            A proxied url of the author icon.
399
400
        :param url:
401
            The url for the author name, this will make the
402
            name field a link/url.
403
404
        :return: self
405
        """
406
407
        self.author = EmbedAuthor(
408
            icon_url=icon_url,
409
            name=name,
410
            proxy_icon_url=proxy_icon_url,
411
            url=url
412
        )
413
414
        return self
415
416
    def set_image(
417
            self,
418
            url: APINullable[str] = MISSING,
419
            proxy_url: APINullable[str] = MISSING,
420
            height: APINullable[int] = MISSING,
421
            width: APINullable[int] = MISSING
422
    ) -> Embed:
423
        """
424
        Sets an image for your embed.
425
426
        :param url:
427
            Source url of the video
428
429
        :param proxy_url:
430
            A proxied url of the video
431
432
        :param height:
433
            Height of the video
434
435
        :param width:
436
            Width of the video
437
438
        :return: self
439
        """
440
        self.image = EmbedImage(
441
            height=height,
442
            url=url,
443
            proxy_url=proxy_url,
444
            width=width
445
        )
446
447
        return self
448
449
    def set_thumbnail(
450
            self,
451
            height: APINullable[int] = MISSING,
452
            url: APINullable[str] = MISSING,
453
            proxy_url: APINullable[str] = MISSING,
454
            width: APINullable[int] = MISSING
455
    ) -> Embed:
456
        """
457
        Sets the thumbnail of the embed.
458
        This image is bigger than the `image` property.
459
460
        :param url:
461
            Source url of the video
462
463
        :param proxy_url:
464
            A proxied url of the video
465
466
        :param height:
467
            Height of the video
468
469
        :param width:
470
            Width of the video
471
472
        :return self:
473
        """
474
        self.thumbnail = EmbedThumbnail(
475
            height=height,
476
            url=url,
477
            proxy_url=proxy_url,
478
            width=width
479
        )
480
481
        return self
482
483
    def set_footer(
484
            self,
485
            text: str,
486
            icon_url: APINullable[str] = MISSING,
487
            proxy_icon_url: APINullable[str] = MISSING
488
    ) -> Embed:
489
        """
490
        Sets the embed footer. This is at the bottom of your embed.
491
492
        :param text:
493
            Footer text
494
495
        :param icon_url:
496
            Url of the footer icon
497
498
        :param proxy_icon_url:
499
            A proxied url of the footer icon
500
501
        :return: self
502
        """
503
        self.footer = EmbedFooter(
504
            text=text,
505
            icon_url=icon_url,
506
            proxy_icon_url=proxy_icon_url
507
        )
508
509
        return self
510
511
    def add_field(
512
            self,
513
            name: str,
514
            value: str,
515
            inline: APINullable[bool] = MISSING
516
    ) -> Embed:
517
        """
518
        Adds a field to the embed.
519
        An embed can contain up to 25 fields.
520
521
        :param name:
522
            The name of the field
523
524
        :param value:
525
            The text in the field
526
527
        :param inline:
528
            Whether or not this field should display inline
529
530
        :raises EmbedFieldError:
531
            Raised when there are more than 25 fields in the embed
532
        """
533
        _field = EmbedField(
534
            name=name,
535
            value=value,
536
            inline=inline
537
        )
538
539
        if len(self.fields) > 25:
540
            raise EmbedFieldError.from_desc(
541
                "Embed field", 25, len(self.fields) + 1
542
            )
543
544
        self.fields += [_field]
545
546
        return self
547
548
    def add_fields(
0 ignored issues
show
best-practice introduced by
Too many arguments (6/5)
Loading history...
549
            self,
550
            field_list: Union[Dict[Any, Any], Iterable[Iterable[Any, Any]]],
551
            checks: Optional[Callable[[Any], Any]] = bool,
552
            map_title: Optional[Callable[[Any], str]] = str,
553
            map_values: Optional[Callable[[Any], str]] = str,
554
            inline: bool = True
555
    ) -> Embed:
556
        """
557
        Add multiple fields from a list,
558
        dict or generator of fields with possible mapping.
559
560
        :param field_list:
561
            A iterable or generator of the fields to add.
562
            If the field_list type is a dictionary, will take items.
563
564
        :param checks:
565
            A filter function to remove embed fields.
566
567
        :param map_title:
568
            A transform function to change the titles.
569
570
        :param map_values:
571
            A transform function to change the values.
572
573
        :param inline:
574
            Whether to create grid or each field on a new line.
575
576
        :raises EmbedFieldError:
577
            Raised when there are more than 25 fields in the embed
578
579
        :return: the embed for chaining methods.
580
        """
581
582
        if isinstance(field_list, dict):
583
            field_list: Iterable[Iterable[Any, Any]] = field_list.items()
584
585
        for field_name, field_value in field_list:
586
            val = (
587
                map_values(field_value)
588
                if not isinstance(field_value, tuple)
589
                else map_values(*field_value)
590
            )
591
592
            if checks(val):
593
                self.add_field(
594
                    name=map_title(field_name),
595
                    value=val,
596
                    inline=inline
597
                )
598
599
        return self
600