Passed
Push — main ( 4bf0cf...a8047f )
by
unknown
01:59 queued 11s
created

Interaction.__post_followup_send_handler()   A

Complexity

Conditions 2

Size

Total Lines 18
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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