Passed
Push — main ( 0eae0e...d518db )
by Yohann
01:23
created

pincer.objects.embed   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 612
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 30
eloc 183
dl 0
loc 612
rs 10
c 0
b 0
f 0

3 Functions

Rating   Name   Duplication   Size   Complexity  
A _field_size() 0 12 2
A _check_if_valid_url() 0 12 2
A _is_valid_url() 0 18 1

13 Methods

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