Passed
Push — main ( 4386e5...bfe7e8 )
by
unknown
02:17
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 collections import defaultdict
7
from dataclasses import dataclass
8
from enum import Enum, IntEnum, IntFlag
9
from typing import TYPE_CHECKING, 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_data import APIDataGen
24
from ...utils.api_object import APIObject, GuildProperty, ChannelProperty
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
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(IntFlag):
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 it's 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
    """  # noqa: E501
327
328
    # Always guaranteed
329
    id: Snowflake
330
    channel_id: Snowflake
331
332
    # Only guaranteed in Message Create event
333
    author: APINullable[User] = MISSING
334
    content: APINullable[str] = MISSING
335
    timestamp: APINullable[Timestamp] = MISSING
336
    tts: APINullable[bool] = MISSING
337
    mention_everyone: APINullable[bool] = MISSING
338
    mentions: APINullable[List[GuildMember]] = MISSING
339
    mention_roles: APINullable[List[Role]] = MISSING
340
    attachments: APINullable[List[Attachment]] = MISSING
341
    embeds: APINullable[List[Embed]] = MISSING
342
    pinned: APINullable[bool] = MISSING
343
    type: APINullable[MessageType] = MISSING
344
345
    # Never guaranteed
346
    edited_timestamp: APINullable[Timestamp] = MISSING
347
    mention_channels: APINullable[List[ChannelMention]] = MISSING
348
    guild_id: APINullable[Snowflake] = MISSING
349
    member: APINullable[GuildMember] = MISSING
350
    reactions: APINullable[List[Reaction]] = MISSING
351
    nonce: APINullable[Union[int, str]] = MISSING
352
    webhook_id: APINullable[Snowflake] = MISSING
353
    activity: APINullable[MessageActivity] = MISSING
354
    application: APINullable[Application] = MISSING
355
    application_id: APINullable[Snowflake] = MISSING
356
    message_reference: APINullable[MessageReference] = MISSING
357
    flags: APINullable[MessageFlags] = MISSING
358
    referenced_message: APINullable[Optional[UserMessage]] = MISSING
359
    interaction: APINullable[MessageInteraction] = MISSING
360
    thread: APINullable[Channel] = MISSING
361
    components: APINullable[List[MessageComponent]] = MISSING
362
    sticker_items: APINullable[List[StickerItem]] = MISSING
363
364
    @classmethod
365
    async def from_id(
366
        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...
367
    ) -> UserMessage:
368
        """|coro|
369
370
        Creates a UserMessage object
371
        It is recommended to use the ``get_message`` function from
372
        :class:`~pincer.client.Client` most of the time.
373
374
        Parameters
375
        ----------
376
        client : :class:`~pincer.client.Client`
377
            Client object to use the HTTP class of.
378
        _id: :class:`~pincer.utils.snowflake.Snowflake`
379
            ID of the message that is wanted.
380
        channel_id : int
381
            ID of the channel the message is in.
382
383
        Returns
384
        -------
385
        :class:`~pincer.objects.message.user_message.UserMessage`
386
            The message object.
387
        """
388
        msg = await client.http.get(f"channels/{channel_id}/messages/{_id}")
389
        return cls.from_dict(msg)
390
391
    async def crosspost(self) -> UserMessage:
392
        """|coro|
393
        Crosspost a message in a News Channel to following channels.
394
395
        This endpoint requires the ``SEND_MESSAGES`` permission,
396
        if the current user sent the message, or additionally the
397
        ``MANAGE_MESSAGES`` permission, for all other messages,
398
        to be present for the current user.
399
400
        Returns
401
        -------
402
        :class:`~pincer.objects.message.UserMessage`
403
            The crossposted message
404
        """
405
        return await self._client.crosspost_message(self.channel_id, self.id)
406
407
    def __str__(self):
408
        return self.content
409
410
    async def get_most_recent(self):
411
        """|coro|
412
413
        Certain Discord methods don't return the message object data after its
414
        updated. This function can be run to get the most recent version of the
415
        message object.
416
        """
417
418
        return self.from_dict(
419
            await self._http.get(
420
                f"/channels/{self.channel_id}/messages/{self.id}"
421
            )
422
        )
423
424
    async def react(self, emoji: str):
425
        """|coro|
426
427
        Create a reaction for the message. Requires the
428
        ``READ_MESSAGE_HISTORY` intent. ``ADD_REACTIONS`` intent is required if
429
        nobody else has reacted using the emoji.
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.put(
438
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}/@me"
439
        )
440
441
    async def unreact(self, emoji: str):
442
        """|coro|
443
444
        Delete a reaction the current user has made for the message.
445
446
        Parameters
447
        ----------
448
        emoji: :class:`str`
449
            Character for emoji. Does not need to be URL encoded.
450
        """
451
452
        await self._http.delete(
453
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}/@me"
454
        )
455
456
    async def remove_user_reaction(self, emoji: str, user_id: Snowflake):
457
        """|coro|
458
459
        Deletes another user's reaction. Requires the ``MANAGE_MESSAGES``
460
        intent.
461
462
        Parameters
463
        ----------
464
        emoji: :class:`str`
465
            Character for emoji. Does not need to be URL encoded.
466
        user_id: :class:`~pincer.utils.snowflake.Snowflake`
467
            User ID
468
        """
469
470
        await self._http.delete(
471
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}"
472
            f"/{user_id}"
473
        )
474
475
    def get_reactions(
476
        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...
477
    ) -> APIDataGen[User]:
478
        # 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...
479
        """|coro|
480
481
        Returns the users that reacted with this emoji.
482
483
        Parameters
484
        ----------
485
        emoji: :class:`str`
486
            Emoji to get users for.
487
        after: :class:`~pincer.utils.snowflake.Snowflake`
488
            Get users after this user ID. Returns all users if not provided.
489
            |default| ``0``
490
        limit: :class:`int`
491
            Max number of users to return (1-100).
492
            |default| ``25``
493
        """
494
        return APIDataGen(
495
            User,
496
            self._http.get(
497
                f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}",
498
                params={"after": after, "limit": limit},
499
            )
500
        )
501
502
    async def remove_all_reactions(self):
503
        """|coro|
504
505
        Delete all reactions on a message. Requires the ``MANAGE_MESSAGES``
506
        intent.
507
        """
508
509
        await self._http.delete(
510
            f"/channels/{self.channel_id}/messages/{self.id}/reactions"
511
        )
512
513
    async def remove_emoji(self, emoji):
514
        """|coro|
515
516
        Deletes all the reactions for a given emoji on a message. Requires the
517
        ``MANAGE_MESSAGES`` intent.
518
519
        Parameters
520
        ----------
521
        emoji: :class:`str`
522
            Character for emoji. Does not need to be URL encoded.
523
        """
524
525
        await self._http.delete(
526
            f"/channels/{self.channel_id}/messages/{self.id}/reactions/{emoji}"
527
        )
528
529
    # TODO: Implement file (https://discord.dev/resources/channel#edit-message)
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
530
    async def edit(
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
531
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
532
        content: str = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
533
        embeds: List[Embed] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
534
        flags: int = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
535
        allowed_mentions: AllowedMentions = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
536
        attachments: List[Attachment] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
537
        components: List[MessageComponent] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
538
    ):
539
        """|coro|
540
541
        Edit a previously sent message. The fields content, embeds, and flags
542
        can be edited by the original message author. Other users can only
543
        edit flags and only if they have the ``MANAGE_MESSAGES`` permission in
544
        the corresponding channel. When specifying flags, ensure to include
545
        all previously set flags/bits in addition to ones that you are
546
        modifying.
547
548
        Parameters
549
        ----------
550
        content: :class:`str`
551
            The message contents (up to 2000 characters)
552
            |default| ``None``
553
        embeds: List[:class:`~pincer.objects.message.embed.Embed`]
554
            Embedded rich content (up to 6000 characters)
555
        flags: :class:`int`
556
            Edit the flags of a message (only ``SUPPRESS_EMBEDS`` can
557
            currently be set/unset)
558
        allowed_mentions: :class:`~pincer.objects.message.message.AllowedMentions`
559
            allowed mentions for the message
560
        attachments: List[:class:`~pincer.objects.message.attachment.Attachment`]
561
            attached files to keep
562
        components: List[:class:`~pincer.objects.message.component.MessageComponent`]
563
            the components to include with the message
564
        """
565
566
        data: DefaultDict[str, JSONSerializable] = defaultdict(list)
567
568
        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...
569
            if isinstance(value, list):
570
                for item in value:
571
                    return set_if_not_none(item, name)
572
573
            if isinstance(value, APIObject):
574
                data[name].append(value.to_dict())
575
576
            elif value is not None:
577
                data[name] = value
578
579
        set_if_not_none(content, "content")
580
        set_if_not_none(embeds, "embeds")
581
        set_if_not_none(flags, "flags")
582
        set_if_not_none(allowed_mentions, "allowed_mentions")
583
        set_if_not_none(attachments, "attachments")
584
        set_if_not_none(components, "components")
585
586
        await self._http.patch(
587
            f"/channels/{self.channel_id}/messages/{self.id}", data=data
588
        )
589
590
    async def delete(self):
591
        """|coro|
592
593
        Delete a message. Requires the ``MANAGE_MESSAGES`` intent if the
594
        message was not sent by the current user.
595
        """
596
597
        await self._http.delete(
598
            f"/channels/{self.channel_id}/messages/{self.id}"
599
        )
600