Interaction.__post_followup_send_handler()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 16
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 16
rs 10
c 0
b 0
f 0
cc 2
nop 3
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 asyncio import sleep, ensure_future
7
from dataclasses import dataclass
8
from typing import Any, Dict, TYPE_CHECKING, Union, Optional, List
9
10
from pincer.utils.api_object import ChannelProperty, GuildProperty
11
from .command_types import AppCommandOptionType
12
from .interaction_base import InteractionType, CallbackType
13
from .mentionable import Mentionable
14
from ..guild.member import GuildMember
15
from ..message.context import MessageContext
16
from ..message.message import Message
17
from ..message.user_message import UserMessage
18
from ..user import User
19
from ...exceptions import (
20
    InteractionDoesNotExist,
21
    UseFollowup,
22
    InteractionAlreadyAcknowledged,
23
    NotFoundError,
24
    InteractionTimedOut,
25
)
26
from ...utils import APIObject
27
from ...utils.convert_message import convert_message
28
from ...utils.snowflake import Snowflake
29
from ...utils.types import MISSING
30
31
if TYPE_CHECKING:
32
    from .interaction_flags import InteractionFlags
33
    from ...utils.convert_message import MessageConvertable
34
    from .command import AppCommandInteractionDataOption
35
    from ..guild.channel import Channel
36
    from ..guild.role import Role
37
    from ...utils import APINullable
38
39
40
@dataclass(repr=False)
41
class ResolvedData(APIObject):
42
    """Represents a Discord Resolved Data structure
43
44
    Attributes
45
    ----------
46
    users: APINullable[Dict[:class:`~pincer.utils.snowflake.Snowflake`, :class:`~pincer.objects.user.user.User`]]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (113/100).

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

Loading history...
47
        Map of Snowflakes to user objects
48
    members: APINullable[Dict[:class:`~pincer.utils.snowflake.Snowflake`, :class:`~pincer.objects.guild.member.GuildMember`]]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (125/100).

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

Loading history...
49
        Map of Snowflakes to partial member objects
50
    roles: APINullable[Dict[:class:`~pincer.utils.snowflake.Snowflake`, :class:`~pincer.objects.guild.role.Role`]]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (114/100).

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

Loading history...
51
        Map of Snowflakes to role objects
52
    channels: APINullable[Dict[:class:`~pincer.utils.snowflake.Snowflake`, :class:`~pincer.objects.guild.channel.Channel`]]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (123/100).

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

Loading history...
53
        Map of Snowflakes to partial channel objects
54
    messages: APINullable[Dict[:class:`~pincer.utils.snowflake.Snowflake`, :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 (134/100).

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

Loading history...
55
        Map of Snowflakes to partial message objects
56
    """
57
58
    # noqa: E501
59
    users: APINullable[Dict[Snowflake, User]] = MISSING
60
    members: APINullable[Dict[Snowflake, GuildMember]] = MISSING
61
    roles: APINullable[Dict[Snowflake, Role]] = MISSING
62
    channels: APINullable[Dict[Snowflake, Channel]] = MISSING
63
    messages: APINullable[Dict[Snowflake, UserMessage]] = MISSING
64
65
66
@dataclass(repr=False)
67
class InteractionData(APIObject):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (9/7)
Loading history...
68
    """Represents a Discord Interaction Data structure
69
70
    Attributes
71
    ----------
72
    id: :class:`~pincer.utils.snowflake.Snowflake`
73
        The `ID` of the invoked command
74
    name: :class:`str`
75
        The `name` of the invoked command
76
    type: :class:`int`
77
        The `type` of the invoked command
78
    resolved: APINullable[:class:`~pincer.objects.app.interactions.ResolvedData`]
79
        Converted users + roles + channels
80
    options: APINullable[:class:`~pincer.objects.app.command.AppCommandInteractionDataOption`]
81
        The params + values from the user
82
    custom_id: APINullable[:class:`str`]
83
        The `custom_id` of the component
84
    component_type: APINullable[:class:`int`]
85
        The type of the component
86
    values: APINullable[:class:`~pincer.objects.app.select_menu.SelectOption`]
87
        The values the user selected
88
    target_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
89
        Id of the user or message targeted by a user or message command
90
    """  # noqa: E501
91
92
    id: APINullable[Snowflake] = MISSING
93
    name: APINullable[str] = MISSING
94
    type: APINullable[int] = MISSING
95
    resolved: APINullable[ResolvedData] = MISSING
96
    options: APINullable[List[AppCommandInteractionDataOption]] = MISSING
97
    custom_id: APINullable[str] = MISSING
98
    component_type: APINullable[int] = MISSING
99
    values: APINullable[List[str]] = MISSING
100
    target_id: APINullable[Snowflake] = MISSING
101
102
103
@dataclass(repr=False)
104
class Interaction(APIObject, ChannelProperty, GuildProperty):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (13/7)
Loading history...
105
    """Represents a Discord Interaction object
106
107
    Attributes
108
    ----------
109
    id: :class:`~pincer.utils.snowflake.Snowflake`
110
        Id of the interaction
111
    application_id: :class:`~pincer.utils.snowflake.Snowflake`
112
        Id of the application this interaction is for
113
    type: :class:`~pincer.objects.app.interaction_base.InteractionType`
114
        The type of interaction
115
    token: :class:`str`
116
        A continuation token for responding to the interaction
117
    version: :class:`int`
118
        Read-only property, always ``1``
119
    data: APINullable[:class:`~pincer.objects.app.interactions.InteractionData`]
120
        The command data payload
121
    guild_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
122
        The guild it was sent from
123
    channel_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
124
        The channel it was sent from
125
    member: APINullable[:class:`~pincer.objects.guild.member.GuildMember`]
126
        Guild member data for the invoking user, including permissions
127
    user: APINullable[:class:`~pincer.objects.user.user.User`]
128
        User object for the invoking user, if invoked in a DM
129
    message: APINullable[:class:`~pincer.objects.message.user_message.UserMessage`]
130
        For components, the message they were attached to
131
    """
132
133
    # noqa: E501
134
    id: Snowflake
135
    application_id: Snowflake
136
    type: InteractionType
137
    token: str
138
139
    version: int = 1
140
    data: APINullable[InteractionData] = MISSING
141
    guild_id: APINullable[Snowflake] = MISSING
142
    channel_id: APINullable[Snowflake] = MISSING
143
    member: APINullable[GuildMember] = MISSING
144
    user: APINullable[User] = MISSING
145
    message: APINullable[UserMessage] = MISSING
146
    has_replied: bool = False
147
    has_acknowledged: bool = False
148
149
    def __post_init__(self):
150
        super().__post_init__()
151
152
        if not self.data.options:
153
            return
154
155
        for option in self.data.options:
156
            if option.type is AppCommandOptionType.STRING:
157
                option.value = str(option.value)
158
            elif option.type is AppCommandOptionType.INTEGER:
159
                option.value = int(option.value)
160
            elif option.type is AppCommandOptionType.BOOLEAN:
161
                option.value = bool(option.value)
162
            elif option.type is AppCommandOptionType.NUMBER:
163
                option.value = float(option.value)
164
165
            elif option.type is AppCommandOptionType.USER:
166
                user = self.return_type(option, self.data.resolved.members)
167
                user.set_user_data(
168
                    self.return_type(option, self.data.resolved.users)
169
                )
170
                option.value = user
171
172
            elif option.type is AppCommandOptionType.CHANNEL:
173
                option.value = self.return_type(
174
                    option, self.data.resolved.channels
175
                )
176
177
            elif option.type is AppCommandOptionType.ROLE:
178
                option.value = self.return_type(
179
                    option, self.data.resolved.roles
180
                )
181
182
            elif option.type is AppCommandOptionType.MENTIONABLE:
183
                user = self.return_type(option, self.data.resolved.members)
184
                if user:
185
                    user.set_user_data(
186
                        self.return_type(option, self.data.resolved.users)
187
                    )
188
189
                option.value = Mentionable(
190
                    user, self.return_type(option, self.data.resolved.roles)
191
                )
192
193
    @staticmethod
194
    def return_type(
195
        option: Snowflake, data: Dict[Snowflake, Any]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
196
    ) -> Optional[APIObject]:
197
        """
198
        Returns a value from the option or None if it doesn't exist.
199
200
        option : :class:`~pincer.utils.types.Snowflake`
201
            Snowflake to search ``data`` for.
202
        data : Dict[:class:`~pincer.utils.types.Snowflake`, Any]
203
            Resolved data to search through.
204
        """
205
        return data[option.value] if data else None
206
207
    def get_message_context(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
208
        return MessageContext(
209
            self._client,
210
            self.member or self.user,
211
            self,
212
            self.guild_id,
213
            self.channel_id,
214
        )
215
216
    async def response(self) -> UserMessage:
217
        """|coro|
218
219
        Gets the original response for an interaction.
220
221
        Returns
222
        -------
223
        :class:`~pincer.objects.message.user_message.UserMessage`
224
            The fetched response!
225
        """
226
        if not self.has_replied:
227
            raise InteractionDoesNotExist(
228
                "No interaction reply has been sent yet!"
229
            )
230
231
        resp = await self._http.get(
232
            f"/webhooks/{self._client.bot.id}/{self.token}/messages/@original"
233
        )
234
        return UserMessage.from_dict(resp)
235
236
    async def _base_ack(
237
        self, flags: Optional[InteractionFlags], callback_type: CallbackType
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
238
    ):
239
        """|coro|
240
241
        Acknowledge an interaction, any flags here are applied to the reply.
242
243
        Parameters
244
        ----------
245
        message_type : :class:`~pincer.objects.app.interaction_base.CallbackType`
246
            The type of the message. Should be ``DEFERRED_MESSAGE`` or
247
            ``DEFERRED_UPDATE_MESSAGE``.
248
        flags: :class:`~pincer.objects.app.interaction_flags.InteractionFlags`
249
            The flags which must be applied to the reply.
250
251
        Raises
252
        ------
253
        :class:`~pincer.exceptions.InteractionAlreadyAcknowledged`
254
            The interaction was already acknowledged, this can be
255
            because a reply or ack was already sent.
256
        """
257
        if self.has_replied or self.has_acknowledged:
258
            raise InteractionAlreadyAcknowledged(
259
                "The interaction you are trying to acknowledge has already "
260
                "been acknowledged"
261
            )
262
263
        self.has_acknowledged = True
264
        await self._http.post(
265
            f"interactions/{self.id}/{self.token}/callback",
266
            {"type": callback_type, "data": {"flags": flags}},
267
        )
268
269
    async def ack(self, flags: Optional[InteractionFlags] = None):
270
        """|coro|
271
272
        Acknowledge an interaction, any flags here are applied to the reply.
273
274
        Parameters
275
        ----------
276
        flags :class:`~pincer.objects.app.interaction_flags.InteractionFlags`
277
            The flags which must be applied to the reply. |default| :data:`None`
278
        """
279
        return await self._base_ack(flags, CallbackType.DEFERRED_MESSAGE)
280
281
    async def deferred_update_ack(
282
        self, flags: Optional[InteractionFlags] = None
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
283
    ):
284
        """|coro|
285
        Same as ack but for updating a message.
286
287
        Parameters
288
        ----------
289
        flags :class:`~pincer.objects.app.interaction_flags.InteractionFlags`
290
            The flags which must be applied to the reply. |default| :data:`None`
291
        """
292
        return await self._base_ack(flags, CallbackType.DEFERRED_UPDATE_MESSAGE)
293
294
    async def __post_send_handler(self, message: Message):
295
        """Process the interaction after it was sent.
296
297
        Parameters
298
        ----------
299
        message :class:`~pincer.objects.message.message.Message`
300
            The interaction message.
301
        """
302
303
        if message.delete_after:
304
            await sleep(message.delete_after)
305
            await self.delete()
306
307
    def __post_sent(self, message: Message):
308
        """Ensure the `__post_send_handler` method its future.
309
310
        Parameters
311
        ----------
312
        message :class:`~pincer.objects.message.message.Message`
313
            The interaction message.
314
        """
315
        self.has_replied = True
316
        ensure_future(self.__post_send_handler(message))
317
318
    async def _base_reply(
319
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
320
        message: MessageConvertable,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
321
        message_type: CallbackType,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
322
        allow_empty: bool,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
323
    ):
324
        """|coro|
325
326
        Initial reply, only works if no ACK has been sent yet.
327
328
        Parameters
329
        ----------
330
        message :class:`~pincer.utils.convert_message.MessageConvertable`
331
            The response message!
332
        message_type : :class:`~pincer.objects.app.interaction_base.CallbackType`
333
            The type of the message. Should be ``MESSAGE`` or
334
            ``UPDATE_MESSAGE``.
335
336
        Raises
337
        ------
338
        :class:`~.pincer.errors.UseFollowup`
339
            Exception raised when a reply has already been sent so a
340
            :func:`~pincer.objects.app.interactions.Interaction.followup`
341
            should be used instead.
342
        :class:`~.pincer.errors.InteractionTimedOut`
343
            Exception raised when discord had to wait too long for a reply.
344
            You can extend the discord wait time by using the
345
            :func:`~pincer.objects.app.interaction.Interaction.ack`
346
            function.
347
        """
348
        if self.has_replied:
0 ignored issues
show
Unused Code introduced by
Unnecessary "elif" after "raise"
Loading history...
349
            raise UseFollowup(
350
                "A response has already been sent to the interaction. "
351
                "Please use a followup instead!"
352
            )
353
        elif self.has_acknowledged:
354
            self.has_replied = True
355
            await self.edit(message)
356
            return
357
358
        message = convert_message(self._client, message)
359
        content_type, data = message.serialize(
360
            message_type=message_type, allow_empty=allow_empty
361
        )
362
363
        try:
364
            await self._http.post(
365
                f"interactions/{self.id}/{self.token}/callback",
366
                data,
367
                content_type=content_type,
368
            )
369
        except NotFoundError:
370
            raise InteractionTimedOut(
371
                "Discord had to wait too long for the interaction reply, "
372
                "you can extend the time it takes for discord to timeout by "
373
                "acknowledging the interaction. (using interaction.ack)"
374
            )
375
376
        self.__post_sent(message)
377
378
    async def reply(self, message: MessageConvertable) -> UserMessage:
379
        """|coro|
380
        Sends a reply to an interaction.
381
        """
382
        return await self._base_reply(message, CallbackType.MESSAGE, False)
383
384
    async def update(self, message: MessageConvertable) -> UserMessage:
385
        """|coro|
386
        Edits the reply to an interaction. Only works with Message Component
387
        Interactions.
388
        """
389
        return await self._base_reply(
390
            message, CallbackType.UPDATE_MESSAGE, True
391
        )
392
393
    async def edit(self, message: MessageConvertable) -> UserMessage:
394
        """|coro|
395
396
        Edit an interaction. This is also the way to reply to
397
        interactions whom have been acknowledged.
398
399
        Parameters
400
        ----------
401
        message :class:`~pincer.utils.convert_message.MessageConvertable`
402
            The new message!
403
404
        Returns
405
        -------
406
        :class:`~pincer.objects.message.user_message.UserMessage`
407
            The updated message object.
408
409
        Raises
410
        ------
411
        :class:`~.pincer.errors.InteractionDoesNotExist`
412
            Exception raised when no reply has been sent.
413
        """
414
415
        if not self.has_replied:
416
            raise InteractionDoesNotExist(
417
                "The interaction whom you are trying to edit has not "
418
                "been sent yet!"
419
            )
420
421
        message = convert_message(self._client, message)
422
        content_type, data = message.serialize()
423
424
        resp = await self._http.patch(
425
            f"webhooks/{self._client.bot.id}/{self.token}/messages/@original",
426
            data,
427
            content_type=content_type,
428
        )
429
        self.__post_sent(message)
430
        return UserMessage.from_dict(resp)
431
432
    async def delete(self):
433
        """|coro|
434
435
        Delete the interaction.
436
437
        Raises
438
        ------
439
        :class:`~pincer.errors.InteractionDoesNotExist`
440
            Exception raised when no reply has been sent.
441
        """
442
        if not self.has_replied:
443
            raise InteractionDoesNotExist(
444
                "The interaction whom you are trying to delete has not "
445
                "been sent yet!"
446
            )
447
448
        await self._http.delete(
449
            f"webhooks/{self._client.bot.id}/{self.token}/messages/@original"
450
        )
451
452
    async def __post_followup_send_handler(
453
        self, followup: UserMessage, message: Message
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
454
    ):
455
        """Process a followup after it was sent.
456
457
        Parameters
458
        ----------
459
        followup :class:`~pincer.objects.message.user_message.UserMessage`
460
            The followup message that is being post processed.
461
        message :class:`~pincer.objects.message.message.Message`
462
            The followup message.
463
        """
464
465
        if message.delete_after:
466
            await sleep(message.delete_after)
467
            await self.delete_followup(followup.id)
468
469
    def __post_followup_sent(self, followup: UserMessage, message: Message):
470
        """Ensure the `__post_followup_send_handler` method its future.
471
472
        Parameters
473
        ----------
474
        followup :class:`~pincer.objects.message.user_message.UserMessage`
475
            The followup message that is being post processed.
476
        message :class:`~pincer.objects.message.message.Message`
477
            The followup message.
478
        """
479
        ensure_future(self.__post_followup_send_handler(followup, message))
480
481
    async def followup(self, message: MessageConvertable) -> UserMessage:
482
        """|coro|
483
484
        Create a follow-up message for the interaction.
485
        This allows you to respond with multiple messages.
486
487
        Parameters
488
        ----------
489
        message :class:`~pincer.utils.convert_message.MessageConvertable`
490
            The message to send.
491
492
        Returns
493
        -------
494
        :class:`~pincer.objects.message.user_message.UserMessage`
495
            The message that has been sent.
496
        """
497
        message = convert_message(self._client, message)
498
        content_type, data = message.serialize()
499
500
        resp = await self._http.post(
501
            f"webhooks/{self._client.bot.id}/{self.token}",
502
            data,
503
            content_type=content_type,
504
        )
505
        msg = UserMessage.from_dict(resp)
506
        self.__post_followup_sent(msg, message)
507
        return msg
508
509
    async def edit_followup(
510
        self, message_id: int, message: MessageConvertable
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
511
    ) -> UserMessage:
512
        """|coro|
513
514
        Edit a followup message.
515
516
        Parameters
517
        ----------
518
        message_id :class:`int`
519
            The id of the original followup message.
520
        message :class:`~pincer.utils.convert_message.MessageConvertable`
521
            The message new message.
522
523
        Returns
524
        -------
525
        :class:`~pincer.objects.message.user_message.UserMessage`
526
            The updated message object.
527
        """
528
        message = convert_message(self._client, message)
529
        content_type, data = message.serialize()
530
531
        resp = await self._http.patch(
532
            f"webhooks/{self._client.bot.id}/{self.token}/messages/{message_id}",
533
            data,
534
            content_type=content_type,
535
        )
536
        msg = UserMessage.from_dict(resp)
537
        self.__post_followup_sent(msg, message)
538
        return msg
539
540
    async def get_followup(self, message_id: int) -> UserMessage:
541
        """|coro|
542
543
        Get a followup message by id.
544
545
        Parameters
546
        ----------
547
        message_id :class:`int`
548
            The id of the original followup message that must be fetched.
549
550
        Returns
551
        -------
552
        :class:`~pincer.objects.message.user_message.UserMessage`
553
            The fetched message object.
554
        """
555
556
        resp = await self._http.get(
557
            f"webhooks/{self._client.bot.id}/{self.token}/messages/{message_id}",
558
        )
559
        return UserMessage.from_dict(resp)
560
561
    async def delete_followup(self, message: Union[UserMessage, int]):
562
        """|coro|
563
564
        Remove a followup message by id.
565
566
        Parameters
567
        ----------
568
        message Union[:class:`~pincer.objects.user_message.UserMessage`, :class:`int`]
569
            The id/followup object of the followup message that must be deleted.
570
        """
571
        message_id = message if isinstance(message, int) else message.id
572
573
        await self._http.delete(
574
            f"webhooks/{self._client.bot.id}/{self.token}/messages/{message_id}",
575
        )
576