Passed
Pull Request — main (#315)
by
unknown
02:19
created

UserMessage.__str__()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
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
from dataclasses import dataclass
7
from enum import Enum, IntEnum
8
from typing import TYPE_CHECKING, DefaultDict
9
from collections import defaultdict
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, GuildProperty, ChannelProperty
24
from ...utils.conversion import construct_client_dict
25
from ...utils.snowflake import Snowflake
26
from ...utils.types import MISSING, JSONSerializable
27
28
if TYPE_CHECKING:
29
    from typing import Any, List, Optional, Union, Generator
0 ignored issues
show
introduced by
Imports from package typing are not grouped
Loading history...
30
31
    from ... import Client
32
    from ..guild.channel import Channel, ChannelMention
33
    from ...utils.types import APINullable
34
    from ...utils.timestamp import Timestamp
35
36
37
class AllowedMentionTypes(str, Enum):
38
    """The allowed mentions.
39
40
    Attributes
41
    ----------
42
    ROLES:
43
        Controls role mentions
44
    USERS:
45
        Controls user mentions
46
    EVERYONE:
47
        Controls @everyone and @here mentions
48
    """
49
50
    ROLES = "roles"
51
    USERS = "users"
52
    EVERYONE = "everyone"
53
54
55
@dataclass(repr=False)
56
class AllowedMentions(APIObject):
57
    """Represents the entities the client can mention
58
59
    Attributes
60
    ----------
61
    parse: List[:class:`~pincer.objects.message.user_message.AllowedMentionTypes`]
62
        An array of allowed mention types to parse from the content.
63
    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...
64
        List of ``Role`` objects or snowflakes of allowed mentions.
65
    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...
66
        List of ``user`` objects or snowflakes of allowed mentions.
67
    reply: :class:`bool`
68
        If replies should mention the author.
69
        |default| :data:`True`
70
    """
71
72
    # noqa: E501
73
74
    parse: List[AllowedMentionTypes]
75
    roles: List[Union[Role, Snowflake]]
76
    users: List[Union[User, Snowflake]]
77
    reply: bool = True
78
79
    def to_dict(self):
80
        def get_str_id(obj: Union[Snowflake, User, Role]) -> str:
81
            if hasattr(obj, "id"):
82
                obj = obj.id
83
84
            return str(obj)
85
86
        return {
87
            "parse": self.parse,
88
            "roles": list(map(get_str_id, self.roles)),
89
            "users": list(map(get_str_id, self.users)),
90
            "replied_user": self.reply,
91
        }
92
93
94
class MessageActivityType(IntEnum):
95
    """The activity people can perform on a rich presence activity.
96
97
    Such an activity could for example be a spotify listen.
98
99
    Attributes
100
    ----------
101
    JOIN:
102
        Invite to join.
103
    SPECTATE:
104
        Invite to spectate.
105
    LISTEN:
106
        Invite to listen along.
107
    JOIN_REQUEST:
108
        Request to join.
109
    """
110
111
    JOIN = 1
112
    SPECTATE = 2
113
    LISTEN = 3
114
    JOIN_REQUEST = 5
115
116
117
class MessageFlags(IntEnum):
118
    """Special message properties.
119
120
    Attributes
121
    ----------
122
    CROSSPOSTED:
123
        The message has been published to subscribed
124
        channels (via Channel Following)
125
    IS_CROSSPOST:
126
        This message originated from a message
127
        in another channel (via Channel Following)
128
    SUPPRESS_EMBEDS:
129
        Do not include any embeds when serializing this message
130
    SOURCE_MESSAGE_DELETED:
131
        The source message for this crosspost
132
        has been deleted (via Channel Following)
133
    URGENT:
134
        This message came from the urgent message system
135
    HAS_THREAD:
136
        This message has an associated thread,
137
        with the same id as the message
138
    EPHEMERAL:
139
        This message is only visible to the user
140
        who invoked the Interaction
141
    LOADING:
142
        This message is an Interaction
143
        Response and the bot is "thinking"
144
    """
145
146
    CROSSPOSTED = 1 << 0
147
    IS_CROSSPOST = 1 << 1
148
    SUPPRESS_EMBEDS = 1 << 2
149
    SOURCE_MESSAGE_DELETED = 1 << 3
150
    URGENT = 1 << 4
151
    HAS_THREAD = 1 << 5
152
    EPHEMERAL = 1 << 6
153
    LOADING = 1 << 7
154
155
156
class MessageType(IntEnum):
157
    """Represents the type of the message.
158
159
    Attributes
160
    ----------
161
    DEFAULT:
162
        Normal message.
163
    RECIPIENT_ADD:
164
        Recipient is added.
165
    RECIPIENT_REMOVE:
166
        Recipient is removed.
167
    CALL:
168
        A call is being made.
169
    CHANNEL_NAME_CHANGE:
170
        The group channel name is changed.
171
    CHANNEL_ICON_CHANGE:
172
        The group channel icon is changed.
173
    CHANNEL_PINNED_MESSAGE:
174
        A message is pinned.
175
    GUILD_MEMBER_JOIN:
176
        A member joined.
177
    USER_PREMIUM_GUILD_SUBSCRIPTION:
178
        A boost.
179
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1:
180
        A boost that reached tier 1.
181
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2:
182
        A boost that reached tier 2.
183
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3:
184
        A boost that reached tier 3.
185
    CHANNEL_FOLLOW_ADD:
186
        A channel is subscribed to.
187
    GUILD_DISCOVERY_DISQUALIFIED:
188
        The guild is disqualified from discovery,
189
    GUILD_DISCOVERY_REQUALIFIED:
190
        The guild is requalified for discovery.
191
    GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING:
192
        Warning about discovery violations.
193
    GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING:
194
        Final warning about discovery violations.
195
    THREAD_CREATED:
196
        A thread is created.
197
    REPLY:
198
        A message reply.
199
    APPLICATION_COMMAND:
200
        Slash command is used and responded to.
201
    THREAD_STARTER_MESSAGE:
202
        The initial message in a thread when its created off a message.
203
    GUILD_INVITE_REMINDER:
204
        ??
205
    """
206
207
    DEFAULT = 0
208
    RECIPIENT_ADD = 1
209
    RECIPIENT_REMOVE = 2
210
    CALL = 3
211
    CHANNEL_NAME_CHANGE = 4
212
    CHANNEL_ICON_CHANGE = 5
213
    CHANNEL_PINNED_MESSAGE = 6
214
    GUILD_MEMBER_JOIN = 7
215
    USER_PREMIUM_GUILD_SUBSCRIPTION = 8
216
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9
217
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10
218
    USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11
219
    CHANNEL_FOLLOW_ADD = 12
220
    GUILD_DISCOVERY_DISQUALIFIED = 14
221
    GUILD_DISCOVERY_REQUALIFIED = 15
222
    GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16
223
    GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17
224
    THREAD_CREATED = 18
225
    REPLY = 19
226
    APPLICATION_COMMAND = 20
227
228
    if GatewayConfig.version < 8:
229
        REPLY = 0
230
        APPLICATION_COMMAND = 0
231
232
    if GatewayConfig.version >= 9:
233
        THREAD_STARTER_MESSAGE = 21
234
235
    GUILD_INVITE_REMINDER = 22
236
    CONTEXT_MENU_COMMAND = 23
237
238
239
@dataclass(repr=False)
240
class MessageActivity(APIObject):
241
    """Represents a Discord Message Activity object
242
243
    Attributes
244
    ----------
245
    type: :class:`~pincer.objects.message.user_message.MessageActivity`
246
        type of message activity
247
    party_id: APINullable[:class:`str`]
248
        party_id from a Rich Presence event
249
    """
250
251
    type: MessageActivityType
252
    party_id: APINullable[str] = MISSING
253
254
255
@dataclass(repr=False)
256
class UserMessage(APIObject, GuildProperty, ChannelProperty):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (30/7)
Loading history...
257
    """Represents a message sent in a channel within Discord.
258
259
    Attributes
260
    ----------
261
    id: :class:`~pincer.utils.snowflake.Snowflake`
262
        Ud of the message
263
    channel_id: :class:`~pincer.utils.snowflake.Snowflake`
264
        Id of the channel the message was sent in
265
    author: :class:`~pincer.objects.user.user.User`
266
        The author of this message (not guaranteed to be a valid user)
267
    content: :class:`str`
268
        Contents of the message
269
    timestamp: :class:`~pincer.utils.timestamp.Timestamp`
270
        When this message was sent
271
    edited_timestamp: Optional[:class:`~pincer.utils.timestamp.Timestamp`]
272
        When this message was edited (or null if never)
273
    tts: :class:`bool`
274
        Whether this was a TTS message
275
    mention_everyone: :class:`bool`
276
        Whether this message mentions everyone
277
    mentions: List[:class:`~pincer.objects.guild.member.GuildMember`]
278
        Users specifically mentioned in the message
279
    mention_roles: List[:class:`~pincer.objects.guild.role.Role`]
280
        Roles specifically mentioned in this message
281
    attachments: List[:class:`~pincer.objects.message.attachment.Attachment`]
282
        Any attached files
283
    embeds: List[:class:`~pincer.objects.message.embed.Embed`]
284
        Any embedded content
285
    pinned: :class:`bool`
286
        Whether this message is pinned
287
    type: :class:`~pincer.objects.message.user_message.MessageType`
288
        Type of message
289
    mention_channels: APINullable[List[:class:`~pincer.objects.guild.channel.Channel`]]
290
        Channels specifically mentioned in this message
291
    guild_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
292
        Id of the guild the message was sent in
293
    member: APINullable[:class:`~pincer.objects.guild.member.PartialGuildMember`]
294
        Member properties for this message's author
295
    reactions: APINullable[List[:class:`~pincer.objects.message.reaction.Reaction`]]
296
        Reactions to the message
297
    nonce: APINullable[Union[:class:`int`, :class:`str`]]
298
        User for validating a message was sent
299
    webhook_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
300
        If the message is generated by a webhook,
301
        this is the webhook's id
302
    activity: APINullable[:class:`~pincer.objects.message.user_message.MessageActivity`]
303
        Sent with Rich Presence-related chat embeds
304
    application: APINullable[:class:`~pincer.objects.app.application.Application`]
305
        Sent with Rich Presence-related chat embeds
306
    application_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
307
        If the message is a response to an Interaction,
308
        this is the id of the interaction's application
309
    message_reference: APINullable[:class:`~pincer.objects.message.reference.MessageReference`]
310
        Data showing the source of a crosspost,
311
        channel follow add, pin, or reply message
312
    flags: APINullable[:class:`~pincer.objects.message.user_message.MessageFlags`]
313
        Message flags combined as a bitfield
314
    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...
315
        The message associated with the message_reference
316
    interaction: APINullable[:class:`~pincer.objects.app.interaction_base.MessageInteraction`]
317
        Sent if the message is a response to an Interaction
318
    thread: APINullable[:class:`~pincer.objects.guild.channel.Channel`]
319
        The thread that was started from this message,
320
        includes thread member object
321
    components: APINullable[List[:class:`~pincer.objects.message.component.MessageComponent`]]
322
        Sent if the message contains components like buttons,
323
        action rows, or other interactive components
324
    sticker_items: APINullable[List[:class:`~pincer.objects.message.sticker.StickerItem`]]
325
        Sent if the message contains stickers
326
    """
327
328
    # noqa: E501
329
330
    id: Snowflake
331
    channel_id: Snowflake
332
    author: User
333
    content: str
334
    timestamp: Timestamp
335
    tts: bool
336
    mention_everyone: bool
337
    mentions: List[GuildMember]
338
    mention_roles: List[Role]
339
    attachments: List[Attachment]
340
    embeds: List[Embed]
341
    pinned: bool
342
    type: MessageType
343
344
    edited_timestamp: APINullable[Timestamp] = MISSING
345
    mention_channels: APINullable[List[ChannelMention]] = MISSING
346
    guild_id: APINullable[Snowflake] = MISSING
347
    member: APINullable[GuildMember] = MISSING
348
    reactions: APINullable[List[Reaction]] = MISSING
349
    nonce: APINullable[Union[int, str]] = MISSING
350
    webhook_id: APINullable[Snowflake] = MISSING
351
    activity: APINullable[MessageActivity] = MISSING
352
    application: APINullable[Application] = MISSING
353
    application_id: APINullable[Snowflake] = MISSING
354
    message_reference: APINullable[MessageReference] = MISSING
355
    flags: APINullable[MessageFlags] = MISSING
356
    referenced_message: APINullable[Optional[UserMessage]] = MISSING
357
    interaction: APINullable[MessageInteraction] = MISSING
358
    thread: APINullable[Channel] = MISSING
359
    components: APINullable[List[MessageComponent]] = MISSING
360
    sticker_items: APINullable[List[StickerItem]] = MISSING
361
362
    @classmethod
363
    async def from_id(
364
        cls, client: Client, _id: Snowflake, channel_id: Snowflake
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
365
    ) -> UserMessage:
366
        """|coro|
367
368
        Creates a UserMessage object
369
        It is recommended to use the ``get_message`` function from
370
        :class:`~pincer.client.Client` most of the time.
371
372
        Parameters
373
        ----------
374
        client : :class:`~pincer.client.Client`
375
            Client object to use the HTTP class of.
376
        _id: :class:`~pincer.utils.snowflake.Snowflake`
377
            ID of the message that is wanted.
378
        channel_id : int
379
            ID of the channel the message is in.
380
381
        Returns
382
        -------
383
        :class:`~pincer.objects.message.user_message.UserMessage`
384
            The message object.
385
        """
386
        msg = await client.http.get(f"channels/{channel_id}/messages/{_id}")
387
        return cls.from_dict(construct_client_dict(client, msg))
388
389
    def __str__(self):
390
        return self.content
391
392
    async def get_most_recent(self):
393
        """|coro|
394
395
        Certain Discord methods don't return the message object data after its
396
        updated. This function can be run to get the most recent version of the
397
        message object.
398
        """
399
400
        return self.from_dict(
401
            construct_client_dict(
402
                self._client,
403
                await self._http.get(
404
                    f"/channels/{self.channel_id}/messages/{self.id}"
405
                ),
406
            )
407
        )
408
409
    async def react(self, emoji: str):
410
        """|coro|
411
412
        Create a reaction for the message. Requires the
413
        ``READ_MESSAGE_HISTORY` intent. ``ADD_REACTIONS`` intent is required if
414
        nobody else has reacted using the emoji.
415
416
        Parameters
417
        ----------
418
        emoji: :class:`str`
419
            Character for emoji. Does not need to be URL encoded.
420
        """
421
422
        await self._http.put(
423
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}/@me"
424
        )
425
426
    async def unreact(self, emoji: str):
427
        """|coro|
428
429
        Delete a reaction the current user has made for the message.
430
431
        Parameters
432
        ----------
433
        emoji: :class:`str`
434
            Character for emoji. Does not need to be URL encoded.
435
        """
436
437
        await self._http.delete(
438
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}/@me"
439
        )
440
441
    async def remove_user_reaction(self, emoji: str, user_id: Snowflake):
442
        """|coro|
443
444
        Deletes another user's reaction. Requires the ``MANAGE_MESSAGES``
445
        intent.
446
447
        Parameters
448
        ----------
449
        emoji: :class:`str`
450
            Character for emoji. Does not need to be URL encoded.
451
        user_id: :class:`~pincer.utils.snowflake.Snowflake`
452
            User ID
453
        """
454
455
        await self._http.delete(
456
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}"
457
            f"/{user_id}"
458
        )
459
460
    async def get_reactions(
461
        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...
462
    ) -> Generator[User, None, None]:
463
        # 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...
464
        """|coro|
465
466
        Returns the users that reacted with this emoji.
467
468
        Parameters
469
        ----------
470
        emoji: :class:`str`
471
            Emoji to get users for.
472
        after: :class:`~pincer.utils.snowflake.Snowflake`
473
            Get users after this user ID. Returns all users if not provided.
474
            |default| ``0``
475
        limit: :class:`int`
476
            Max number of users to return (1-100).
477
            |default| ``25``
478
        """
479
480
        for user in await self._http.get(
481
            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...
482
            f"?after={after}&limit={limit}"
483
        ):
484
            yield User.from_dict(user)
485
486
    async def remove_all_reactions(self):
487
        """|coro|
488
489
        Delete all reactions on a message. Requires the ``MANAGE_MESSAGES``
490
        intent.
491
        """
492
493
        await self._http.delete(
494
            f"/channels/{self.channel_id}/messages/{self.id}/reactions"
495
        )
496
497
    async def remove_emoji(self, emoji):
498
        """|coro|
499
500
        Deletes all the reactions for a given emoji on a message. Requires the
501
        ``MANAGE_MESSAGES`` intent.
502
503
        Parameters
504
        ----------
505
        emoji: :class:`str`
506
            Character for emoji. Does not need to be URL encoded.
507
        """
508
509
        await self._http.delete(
510
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}"
511
        )
512
513
    # 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...
514
    async def edit(
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
515
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
516
        content: str = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
517
        embeds: List[Embed] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
518
        flags: int = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
519
        allowed_mentions: AllowedMentions = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
520
        attachments: List[Attachment] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
521
        components: List[MessageComponent] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
522
    ):
523
        """|coro|
524
525
        Edit a previously sent message. The fields content, embeds, and flags
526
        can be edited by the original message author. Other users can only
527
        edit flags and only if they have the ``MANAGE_MESSAGES`` permission in
528
        the corresponding channel. When specifying flags, ensure to include
529
        all previously set flags/bits in addition to ones that you are
530
        modifying.
531
532
        Parameters
533
        ----------
534
        content: :class:`str`
535
            The message contents (up to 2000 characters)
536
            |default| ``None``
537
        embeds: List[:class:`~pincer.objects.message.embed.Embed`]
538
            Embedded rich content (up to 6000 characters)
539
        flags: :class:`int`
540
            Edit the flags of a message (only ``SUPPRESS_EMBEDS`` can
541
            currently be set/unset)
542
        allowed_mentions: :class:`~pincer.objects.message.message.AllowedMentions`
543
            allowed mentions for the message
544
        attachments: List[:class:`~pincer.objects.message.attachment.Attachment`]
545
            attached files to keep
546
        components: List[:class:`~pincer.objects.message.component.MessageComponent`]
547
            the components to include with the message
548
        """
549
550
        data: DefaultDict[str, JSONSerializable] = defaultdict(list)
551
552
        def set_if_not_none(value: Any, name: str):
0 ignored issues
show
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
553
            if isinstance(value, list):
554
                for item in value:
555
                    return set_if_not_none(item, name)
556
557
            if isinstance(value, APIObject):
558
                data[name].append(value.to_dict())
559
560
            elif value is not None:
561
                data[name] = value
562
563
        set_if_not_none(content, "content")
564
        set_if_not_none(embeds, "embeds")
565
        set_if_not_none(flags, "flags")
566
        set_if_not_none(allowed_mentions, "allowed_mentions")
567
        set_if_not_none(attachments, "attachments")
568
        set_if_not_none(components, "components")
569
570
        await self._http.patch(
571
            f"/channels/{self.channel_id}/messages/{self.id}", data=data
572
        )
573
574
    async def delete(self):
575
        """|coro|
576
577
        Delete a message. Requires the ``MANAGE_MESSAGES`` intent if the
578
        message was not sent by the current user.
579
        """
580
581
        await self._http.delete(
582
            f"/channels/{self.channel_id}/messages/{self.id}"
583
        )
584