Passed
Pull Request — main (#237)
by Yohann
02:03
created

UserMessage.delete()   A

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 1
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
import re
7
from dataclasses import dataclass
8
from enum import Enum, IntEnum
9
from typing import TYPE_CHECKING
10
11
from .attachment import Attachment
12
from .component import MessageComponent
13
from .embed import Embed
14
from .reaction import Reaction
15
from .reference import MessageReference
16
from .sticker import StickerItem
17
from ..app.application import Application
18
from ..app.interaction_base import MessageInteraction
19
from ..guild.member import GuildMember
20
from ..guild.role import Role
21
from ..user.user import User
22
from ..._config import GatewayConfig
23
from ...utils.api_object import APIObject
24
from ...utils.conversion import construct_client_dict
25
from ...utils.snowflake import Snowflake
26
from ...utils.types import MISSING
27
28
if TYPE_CHECKING:
29
    from typing import Any, List, Optional, Union, Generator
30
31
    from ..guild.channel import Channel, ChannelMention
32
    from ...utils.types import APINullable
33
    from ...utils.timestamp import Timestamp
34
35
36
MARKDOWN_PATTERNS = (
37
    re.compile(r"\*\*(.*?)\*\*"),  # bold
38
    re.compile(r"\*(.*?)\*"),  # italic
39
    re.compile(r"_(.*?)_"),  # italic2
40
    re.compile(r"\*\*\*(.*?)\*\*\*"),  # bold+italic
41
    re.compile(r"\_\_(.*?)\_\_"),  # underline
42
    re.compile(r"~~(.*?)~~"),  # crossed
43
    re.compile(r"`?`(.*?)`?`"),  # small code blocks
44
    re.compile(r"\|\|(.*?)\|\|")  # spoilers
45
)
46
47
48
class AllowedMentionTypes(str, Enum):
49
    """The allowed mentions.
50
51
    Attributes
52
    ----------
53
    ROLES:
54
        Controls role mentions
55
    USERS:
56
        Controls user mentions
57
    EVERYONE:
58
        Controls @everyone and @here mentions
59
    """
60
    ROLES = "roles"
61
    USERS = "users"
62
    EVERYONE = "everyone"
63
64
65
@dataclass
66
class AllowedMentions(APIObject):
67
    """Represents the entities the client can mention
68
69
    Attributes
70
    ----------
71
    parse: List[:class:`~pincer.objects.message.user_message.AllowedMentionTypes`]
72
        An array of allowed mention types to parse from the content.
73
    roles: List[Union[:class:`~pincer.objects.guild.role.Role`, :class:`~pincer.utils.snowflake.Snowflake`]]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (108/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
74
        List of ``Role`` objects or snowflakes of allowed mentions.
75
    users: List[Union[:class:`~pincer.objects.user.user.User` :class:`~pincer.utils.snowflake.Snowflake`]]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (106/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
76
        List of ``user`` objects or snowflakes of allowed mentions.
77
    reply: :class:`bool`
78
        If replies should mention the author.
79
        |default| :data:`True`
80
    """
81
    # noqa: E501
82
83
    parse: List[AllowedMentionTypes]
84
    roles: List[Union[Role, Snowflake]]
85
    users: List[Union[User, Snowflake]]
86
    reply: bool = True
87
88
    def to_dict(self):
89
        def get_str_id(obj: Union[Snowflake, User, Role]) -> str:
90
            if hasattr(obj, "id"):
91
                obj = obj.id
92
93
            return str(obj)
94
95
        return {
96
            "parse": self.parse,
97
            "roles": list(map(get_str_id, self.roles)),
98
            "users": list(map(get_str_id, self.users)),
99
            "replied_user": self.reply
100
        }
101
102
103
class MessageActivityType(IntEnum):
104
    """The activity people can perform on a rich presence activity.
105
106
    Such an activity could for example be a spotify listen.
107
108
    Attributes
109
    ----------
110
    JOIN:
111
        Invite to join.
112
    SPECTATE:
113
        Invite to spectate.
114
    LISTEN:
115
        Invite to listen along.
116
    JOIN_REQUEST:
117
        Request to join.
118
    """
119
    JOIN = 1
120
    SPECTATE = 2
121
    LISTEN = 3
122
    JOIN_REQUEST = 5
123
124
125
class MessageFlags(IntEnum):
126
    """Special message properties.
127
128
    Attributes
129
    ----------
130
    CROSSPOSTED:
131
        The message has been published to subscribed
132
        channels (via Channel Following)
133
    IS_CROSSPOST:
134
        This message originated from a message
135
        in another channel (via Channel Following)
136
    SUPPRESS_EMBEDS:
137
        Do not include any embeds when serializing this message
138
    SOURCE_MESSAGE_DELETED:
139
        The source message for this crosspost
140
        has been deleted (via Channel Following)
141
    URGENT:
142
        This message came from the urgent message system
143
    HAS_THREAD:
144
        This message has an associated thread,
145
        with the same id as the message
146
    EPHEMERAL:
147
        This message is only visible to the user
148
        who invoked the Interaction
149
    LOADING:
150
        This message is an Interaction
151
        Response and the bot is "thinking"
152
    """
153
    CROSSPOSTED = 1 << 0
154
    IS_CROSSPOST = 1 << 1
155
    SUPPRESS_EMBEDS = 1 << 2
156
    SOURCE_MESSAGE_DELETED = 1 << 3
157
    URGENT = 1 << 4
158
    HAS_THREAD = 1 << 5
159
    EPHEMERAL = 1 << 6
160
    LOADING = 1 << 7
161
162
163
class MessageType(IntEnum):
164
    """Represents the type of the message.
165
166
    Attributes
167
    ----------
168
    DEFAULT:
169
        Normal message.
170
    RECIPIENT_ADD:
171
        Recipient is added.
172
    RECIPIENT_REMOVE:
173
        Recipient is removed.
174
    CALL:
175
        A call is being made.
176
    CHANNEL_NAME_CHANGE:
177
        The group channel name is changed.
178
    CHANNEL_ICON_CHANGE:
179
        The group channel icon is changed.
180
    CHANNEL_PINNED_MESSAGE:
181
        A message is pinned.
182
    GUILD_MEMBER_JOIN:
183
        A member joined.
184
    USER_PREMIUM_GUILD_SUBSCRIPTION:
185
        A boost.
186
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1:
187
        A boost that reached tier 1.
188
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2:
189
        A boost that reached tier 2.
190
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3:
191
        A boost that reached tier 3.
192
    CHANNEL_FOLLOW_ADD:
193
        A channel is subscribed to.
194
    GUILD_DISCOVERY_DISQUALIFIED:
195
        The guild is disqualified from discovery,
196
    GUILD_DISCOVERY_REQUALIFIED:
197
        The guild is requalified for discovery.
198
    GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING:
199
        Warning about discovery violations.
200
    GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING:
201
        Final warning about discovery violations.
202
    THREAD_CREATED:
203
        A thread is created.
204
    REPLY:
205
        A message reply.
206
    APPLICATION_COMMAND:
207
        Slash command is used and responded to.
208
    THREAD_STARTER_MESSAGE:
209
        The initial message in a thread when its created off a message.
210
    GUILD_INVITE_REMINDER:
211
        ??
212
    """
213
    DEFAULT = 0
214
    RECIPIENT_ADD = 1
215
    RECIPIENT_REMOVE = 2
216
    CALL = 3
217
    CHANNEL_NAME_CHANGE = 4
218
    CHANNEL_ICON_CHANGE = 5
219
    CHANNEL_PINNED_MESSAGE = 6
220
    GUILD_MEMBER_JOIN = 7
221
    USER_PREMIUM_GUILD_SUBSCRIPTION = 8
222
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9
223
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10
224
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11
225
    CHANNEL_FOLLOW_ADD = 12
226
    GUILD_DISCOVERY_DISQUALIFIED = 14
227
    GUILD_DISCOVERY_REQUALIFIED = 15
228
    GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16
229
    GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17
230
    THREAD_CREATED = 18
231
    REPLY = 19
232
    APPLICATION_COMMAND = 20
233
234
    if GatewayConfig.version < 8:
235
        REPLY = 0
236
        APPLICATION_COMMAND = 0
237
238
    if GatewayConfig.version >= 9:
239
        THREAD_STARTER_MESSAGE = 21
240
241
    GUILD_INVITE_REMINDER = 22
242
    CONTEXT_MENU_COMMAND = 23
243
244
245
@dataclass
246
class MessageActivity(APIObject):
247
    """Represents a Discord Message Activity object
248
249
    Attributes
250
    ----------
251
    type: :class:`~pincer.objects.message.user_message.MessageActivity`
252
        type of message activity
253
    party_id: APINullable[:class:`str`]
254
        party_id from a Rich Presence event
255
    """
256
    type: MessageActivityType
257
    party_id: APINullable[str] = MISSING
258
259
260
@dataclass
261
class UserMessage(APIObject):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (30/7)
Loading history...
262
    """Represents a message sent in a channel within Discord.
263
264
    Attributes
265
    ----------
266
    id: :class:`~pincer.utils.snowflake.Snowflake`
267
        Ud of the message
268
    channel_id: :class:`~pincer.utils.snowflake.Snowflake`
269
        Id of the channel the message was sent in
270
    author: :class:`~pincer.objects.user.user.User`
271
        The author of this message (not guaranteed to be a valid user)
272
    content: :class:`str`
273
        Contents of the message
274
    timestamp: :class:`~pincer.utils.timestamp.Timestamp`
275
        When this message was sent
276
    edited_timestamp: Optional[:class:`~pincer.utils.timestamp.Timestamp`]
277
        When this message was edited (or null if never)
278
    tts: :class:`bool`
279
        Whether this was a TTS message
280
    mention_everyone: :class:`bool`
281
        Whether this message mentions everyone
282
    mentions: List[:class:`~pincer.objects.guild.member.GuildMember`]
283
        Users specifically mentioned in the message
284
    mention_roles: List[:class:`~pincer.objects.guild.role.Role`]
285
        Roles specifically mentioned in this message
286
    attachments: List[:class:`~pincer.objects.message.attachment.Attachment`]
287
        Any attached files
288
    embeds: List[:class:`~pincer.objects.message.embed.Embed`]
289
        Any embedded content
290
    pinned: :class:`bool`
291
        Whether this message is pinned
292
    type: :class:`~pincer.objects.message.user_message.MessageType`
293
        Type of message
294
    mention_channels: APINullable[List[:class:`~pincer.objects.guild.channel.Channel`]]
295
        Channels specifically mentioned in this message
296
    guild_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
297
        Id of the guild the message was sent in
298
    member: APINullable[:class:`~pincer.objects.guild.member.PartialGuildMember`]
299
        Member properties for this message's author
300
    reactions: APINullable[List[:class:`~pincer.objects.message.reaction.Reaction`]]
301
        Reactions to the message
302
    nonce: APINullable[Union[:class:`int`, :class:`str`]]
303
        User for validating a message was sent
304
    webhook_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
305
        If the message is generated by a webhook,
306
        this is the webhook's id
307
    activity: APINullable[:class:`~pincer.objects.message.user_message.MessageActivity`]
308
        Sent with Rich Presence-related chat embeds
309
    application: APINullable[:class:`~pincer.objects.app.application.Application`]
310
        Sent with Rich Presence-related chat embeds
311
    application_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
312
        If the message is a response to an Interaction,
313
        this is the id of the interaction's application
314
    message_reference: APINullable[:class:`~pincer.objects.message.reference.MessageReference`]
315
        Data showing the source of a crosspost,
316
        channel follow add, pin, or reply message
317
    flags: APINullable[:class:`~pincer.objects.message.user_message.MessageFlags`]
318
        Message flags combined as a bitfield
319
    referenced_message: APINullable[Optional[:class:`~pincer.objects.message.user_message.UserMessage`]]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (104/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
320
        The message associated with the message_reference
321
    interaction: APINullable[:class:`~pincer.objects.app.interaction_base.MessageInteraction`]
322
        Sent if the message is a response to an Interaction
323
    thread: APINullable[:class:`~pincer.objects.guild.channel.Channel`]
324
        The thread that was started from this message,
325
        includes thread member object
326
    components: APINullable[List[:class:`~pincer.objects.message.component.MessageComponent`]]
327
        Sent if the message contains components like buttons,
328
        action rows, or other interactive components
329
    sticker_items: APINullable[List[:class:`~pincer.objects.message.sticker.StickerItem`]]
330
        Sent if the message contains stickers
331
    """
332
    # noqa: E501
333
334
    id: Snowflake
335
    channel_id: Snowflake
336
    author: User
337
    content: str
338
    timestamp: Timestamp
339
    tts: bool
340
    mention_everyone: bool
341
    mentions: List[GuildMember]
342
    mention_roles: List[Role]
343
    attachments: List[Attachment]
344
    embeds: List[Embed]
345
    pinned: bool
346
    type: MessageType
347
348
    edited_timestamp: APINullable[Timestamp] = MISSING
349
    mention_channels: APINullable[List[ChannelMention]] = MISSING
350
    guild_id: APINullable[Snowflake] = MISSING
351
    member: APINullable[GuildMember] = MISSING
352
    reactions: APINullable[List[Reaction]] = MISSING
353
    nonce: APINullable[Union[int, str]] = MISSING
354
    webhook_id: APINullable[Snowflake] = MISSING
355
    activity: APINullable[MessageActivity] = MISSING
356
    application: APINullable[Application] = MISSING
357
    application_id: APINullable[Snowflake] = MISSING
358
    message_reference: APINullable[MessageReference] = MISSING
359
    flags: APINullable[MessageFlags] = MISSING
360
    referenced_message: APINullable[Optional[UserMessage]] = MISSING
361
    interaction: APINullable[MessageInteraction] = MISSING
362
    thread: APINullable[Channel] = MISSING
363
    components: APINullable[List[MessageComponent]] = MISSING
364
    sticker_items: APINullable[List[StickerItem]] = MISSING
365
366
    def __str__(self):
367
        return self.content
368
369
    async def get_most_recent(self):
370
        """|coro|
371
372
        Certain Discord methods don't return the message object data after its
373
        updated. This function can be run to get the most recent version of the
374
        message object.
375
        """
376
377
        return self.from_dict(
378
            construct_client_dict(
379
                self._client,
380
                await self._http.get(
381
                    f"/channels/{self.channel_id}/messages/{self.id}"
382
                )
383
            )
384
        )
385
386
    async def react(self, emoji: str):
387
        """|coro|
388
389
        Create a reaction for the message. Requires the
390
        ``READ_MESSAGE_HISTORY` intent. ``ADD_REACTIONS`` intent is required if
391
        nobody else has reacted using the emoji.
392
393
        Parameters
394
        ----------
395
        emoji: :class:`str`
396
            Character for emoji. Does not need to be URL encoded.
397
        """
398
399
        await self._http.put(
400
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}/@me"
401
        )
402
403
    async def unreact(self, emoji: str):
404
        """|coro|
405
406
        Delete a reaction the current user has made for the message.
407
408
        Parameters
409
        ----------
410
        emoji: :class:`str`
411
            Character for emoji. Does not need to be URL encoded.
412
        """
413
414
        await self._http.delete(
415
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}/@me"
416
        )
417
418
    async def remove_user_reaction(self, emoji: str, user_id: Snowflake):
419
        """|coro|
420
421
        Deletes another user's reaction. Requires the ``MANAGE_MESSAGES``
422
        intent.
423
424
        Parameters
425
        ----------
426
        emoji: :class:`str`
427
            Character for emoji. Does not need to be URL encoded.
428
        user_id: :class:`~pincer.utils.snowflake.Snowflake`
429
            User ID
430
        """
431
432
        await self._http.delete(
433
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}"
434
            f"/{user_id}"
435
        )
436
437
    async def get_reactions(
438
        self, emoji: str, after: Snowflake = 0, limit=25
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
439
    ) -> Generator[User, None, None]:
440
        # TODO: HTTP Client will need to refactored to allow parameters using aiohttp's system.
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
441
        """|coro|
442
443
        Returns the users that reacted with this emoji.
444
445
        Parameters
446
        ----------
447
        emoji: :class:`str`
448
            Emoji to get users for.
449
        after: :class:`~pincer.utils.snowflake.Snowflake`
450
            Get users after this user ID. Returns all users if not provided.
451
            |default| ``0``
452
        limit: :class:`int`
453
            Max number of users to return (1-100).
454
            |default| ``25``
455
        """
456
457
        for user in await self._http.get(
458
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}"
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
459
            f"?after={after}&limit={limit}"
460
        ):
461
            yield User.from_dict(user)
462
463
    async def remove_all_reactions(self):
464
        """|coro|
465
466
        Delete all reactions on a message. Requires the ``MANAGE_MESSAGES``
467
        intent.
468
        """
469
470
        await self._http.delete(
471
            f"/channels/{self.channel_id}/messages/{self.id}/reactions"
472
        )
473
474
    async def remove_emoji(self, emoji):
475
        """|coro|
476
477
        Deletes all the reactions for a given emoji on a message. Requires the
478
        ``MANAGE_MESSAGES`` intent.
479
480
        Parameters
481
        ----------
482
        emoji: :class:`str`
483
            Character for emoji. Does not need to be URL encoded.
484
        """
485
486
        await self._http.delete(
487
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}"
488
        )
489
490
    # TODO: Implement file (https://discord.com/developers/docs/resources/channel#edit-message)
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
491
    async def edit(
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
492
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
493
        content: str = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
494
        embeds: List[Embed] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
495
        flags: int = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
496
        allowed_mentions: AllowedMentions = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
497
        attachments: List[Attachment] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
498
        components: List[MessageComponent] = None
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
499
    ):
500
        """|coro|
501
502
        Edit a previously sent message. The fields content, embeds, and flags
503
        can be edited by the original message author. Other users can only
504
        edit flags and only if they have the ``MANAGE_MESSAGES`` permission in
505
        the corresponding channel. When specifying flags, ensure to include
506
        all previously set flags/bits in addition to ones that you are
507
        modifying.
508
509
        Parameters
510
        ----------
511
        content: :class:`str`
512
            The message contents (up to 2000 characters)
513
            |default| ``None``
514
        embeds: List[:class:`~pincer.objects.message.embed.Embed`]
515
            Embedded rich content (up to 6000 characters)
516
        flags: :class:`int`
517
            Edit the flags of a message (only ``SUPPRESS_EMBEDS`` can
518
            currently be set/unset)
519
        allowed_mentions: :class:`~pincer.objects.message.message.AllowedMentions`
520
            allowed mentions for the message
521
        attachments: List[:class:`~pincer.objects.message.attachment.Attachment`]
522
            attached files to keep
523
        components: List[:class:`~pincer.objects.message.component.MessageComponent`]
524
            the components to include with the message
525
        """
526
527
        data = {}
528
529
        def set_if_not_none(value: Any, name: str):
530
            if isinstance(value, APIObject):
531
                data[name] = value.to_dict()
532
            elif value is not None:
533
                data[name] = value
534
535
        set_if_not_none(content, "content")
536
        set_if_not_none(embeds, "embeds")
537
        set_if_not_none(flags, "flags")
538
        set_if_not_none(allowed_mentions, "allowed_mentions")
539
        set_if_not_none(attachments, "attachments")
540
        set_if_not_none(components, "components")
541
542
        await self._http.patch(
543
            f"/channels/{self.channel_id}/messages/{self.id}",
544
            data=data
545
        )
546
547
    async def delete(self):
548
        """|coro|
549
550
        Delete a message. Requires the ``MANAGE_MESSAGES`` intent if the
551
        message was not sent by the current user.
552
        """
553
554
        await self._http.delete(
555
            f"/channels/{self.channel_id}/messages/{self.id}"
556
        )
557
558
    @property
559
    def clean_content(self):
560
        """
561
        The message content with any special characters removed.
562
        """
563
        new_content = self.content
564
565
        for pattern in MARKDOWN_PATTERNS:
566
            new_content = re.sub(pattern, r"\1", new_content)
567
568
        return re.sub(
569
            re.compile(r"(.*?)```[a-zA-Z]+(\s*)+\n((?:.|\s)*?)```"),
570
            r"\1\2\3",
571
            new_content
572
        )
573