Passed
Pull Request — main (#253)
by
unknown
01:34
created

pincer.objects.app.interactions.Interaction.ack()   A

Complexity

Conditions 3

Size

Total Lines 29
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 29
rs 9.85
c 0
b 0
f 0
cc 3
nop 2
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 contextlib import suppress
8
from dataclasses import dataclass
9
from typing import Any, Dict, TYPE_CHECKING, Type, Union, Optional, List, T
0 ignored issues
show
Unused Code introduced by
Unused Type imported from typing
Loading history...
Unused Code introduced by
Unused T imported from typing
Loading history...
10
11
from .command_types import AppCommandOptionType
12
from .interaction_base import InteractionType, CallbackType
13
from .mentionable import Mentionable
14
from ..app.select_menu import SelectOption
15
from ..guild.member import GuildMember
16
from ..message.context import MessageContext
17
from ..message.message import Message
18
from ..message.user_message import UserMessage
19
from ..user import User
20
from ...exceptions import InteractionDoesNotExist, UseFollowup, \
21
    InteractionAlreadyAcknowledged, NotFoundError, InteractionTimedOut
22
from ...utils import APIObject
23
from ...utils.convert_message import convert_message
24
from ...utils.snowflake import Snowflake
25
from ...utils.types import MISSING
26
27
if TYPE_CHECKING:
28
    from .interaction_flags import InteractionFlags
29
    from ...utils.convert_message import MessageConvertable
30
    from .command import AppCommandInteractionDataOption
31
    from ..guild.channel import Channel
32
    from ..guild.role import Role
33
    from ...utils import APINullable
34
35
36
@dataclass
37
class ResolvedData(APIObject):
38
    """Represents a Discord Resolved Data structure
39
40
    Attributes
41
    ----------
42
    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...
43
        Map of Snowflakes to user objects
44
    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...
45
        Map of Snowflakes to partial member objects
46
    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...
47
        Map of Snowflakes to role objects
48
    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...
49
        Map of Snowflakes to partial channel objects
50
    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...
51
        Map of Snowflakes to partial message objects
52
    """
53
    # noqa: E501
54
    users: APINullable[Dict[Snowflake, User]] = MISSING
55
    members: APINullable[Dict[Snowflake, GuildMember]] = MISSING
56
    roles: APINullable[Dict[Snowflake, Role]] = MISSING
57
    channels: APINullable[Dict[Snowflake, Channel]] = MISSING
58
    messages: APINullable[Dict[Snowflake, UserMessage]] = MISSING
59
60
61
@dataclass
62
class InteractionData(APIObject):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (9/7)
Loading history...
63
    """Represents a Discord Interaction Data structure
64
65
    Attributes
66
    ----------
67
    id: :class:`~pincer.utils.snowflake.Snowflake`
68
        The `ID` of the invoked command
69
    name: :class:`str`
70
        The `name` of the invoked command
71
    type: :class:`int`
72
        The `type` of the invoked command
73
    resolved: APINullable[:class:`~pincer.objects.app.interactions.ResolvedData`]
74
        Converted users + roles + channels
75
    options: APINullable[:class:`~pincer.objects.app.command.AppCommandInteractionDataOption`]
76
        The params + values from the user
77
    custom_id: APINullable[:class:`str`]
78
        The `custom_id` of the component
79
    component_type: APINullable[:class:`int`]
80
        The type of the component
81
    values: APINullable[:class:`~pincer.objects.app.select_menu.SelectOption`]
82
        The values the user selected
83
    target_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
84
        Id of the user or message targeted by a user or message command
85
    """
86
    # noqa: E501
87
    id: Snowflake
88
    name: str
89
    type: int
90
91
    resolved: APINullable[ResolvedData] = MISSING
92
    options: APINullable[List[AppCommandInteractionDataOption]] = MISSING
93
    custom_id: APINullable[str] = MISSING
94
    component_type: APINullable[int] = MISSING
95
    values: APINullable[SelectOption] = MISSING
96
    target_id: APINullable[Snowflake] = MISSING
97
98
99
@dataclass
100
class Interaction(APIObject):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (13/7)
Loading history...
101
    """Represents a Discord Interaction object
102
103
    Attributes
104
    ----------
105
    id: :class:`~pincer.utils.snowflake.Snowflake`
106
        Id of the interaction
107
    application_id: :class:`~pincer.utils.snowflake.Snowflake`
108
        Id of the application this interaction is for
109
    type: :class:`~pincer.objects.app.interaction_base.InteractionType`
110
        The type of interaction
111
    token: :class:`str`
112
        A continuation token for responding to the interaction
113
    version: :class:`int`
114
        Read-only property, always ``1``
115
    data: APINullable[:class:`~pincer.objects.app.interactions.InteractionData`]
116
        The command data payload
117
    guild_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
118
        The guild it was sent from
119
    channel_id: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
120
        The channel it was sent from
121
    member: APINullable[:class:`~pincer.objects.guild.member.GuildMember`]
122
        Guild member data for the invoking user, including permissions
123
    user: APINullable[:class:`~pincer.objects.user.user.User`]
124
        User object for the invoking user, if invoked in a DM
125
    message: APINullable[:class:`~pincer.objects.message.user_message.UserMessage`]
126
        For components, the message they were attached to
127
    """
128
    # noqa: E501
129
    id: Snowflake
130
    application_id: Snowflake
131
    type: InteractionType
132
    token: str
133
134
    version: int = 1
135
    data: APINullable[InteractionData] = MISSING
136
    guild_id: APINullable[Snowflake] = MISSING
137
    channel_id: APINullable[Snowflake] = MISSING
138
    member: APINullable[GuildMember] = MISSING
139
    user: APINullable[User] = MISSING
140
    message: APINullable[UserMessage] = MISSING
141
    has_replied: bool = False
142
    has_acknowledged: bool = False
143
144
    def __post_init__(self):
145
        super().__post_init__()
146
147
        for option in self.data.options:
148
149
            if option.type is AppCommandOptionType.STRING:
150
                option.value = str(option.value)
151
            elif option.type is AppCommandOptionType.INTEGER:
152
                option.value = int(option.value)
153
            elif option.type is AppCommandOptionType.BOOLEAN:
154
                option.value = bool(option.value)
155
            elif option.type is AppCommandOptionType.NUMBER:
156
                option.value = float(option.value)
157
158
            elif option.type is AppCommandOptionType.USER:
159
                user = self.return_type(option, self.data.resolved.members)
160
                user.set_user_data(
161
                    self.return_type(option, self.data.resolved.users)
162
                )
163
                option.value = user
164
165
            elif option.type is AppCommandOptionType.CHANNEL:
166
                option.value = self.return_type(
167
                    option, self.data.resolved.channels
168
                )
169
170
            elif option.type is AppCommandOptionType.ROLE:
171
                option.value = self.return_type(
172
                    option, self.data.resolved.roles
173
                )
174
175
            elif option.type is AppCommandOptionType.MENTIONABLE:
176
                user = self.return_type(option, self.data.resolved.members)
177
                if user:
178
                    user.set_user_data(self.return_type(
179
                        option, self.data.resolved.users)
180
                    )
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation.
Loading history...
181
182
                option.value = Mentionable(
183
                    user,
184
                    self.return_type(
185
                        option, self.data.resolved.roles
186
                    )
187
                )
188
189
    def return_type(
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
190
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
191
        option: Snowflake,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
192
        data: Dict[Snowflake, Any]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
193
    ) -> APIObject:
194
        """
195
        Returns a value from the option or None if it doesn't exist.
196
197
        option : :class:`~pincer.utils.types.Snowflake`
198
            Snowflake to search ``data`` for.
199
        data : Dict[:class:`~pincer.utils.types.Snowflake`, Any]
200
            Resolved data to search through.
201
        """
202
        with suppress(TypeError, KeyError):
203
            return data[option.value]
204
205
        return None
206
207
    def convert_to_message_context(self, command):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
208
        return MessageContext(
209
            self.member or self.user,
210
            command,
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 ack(self, flags: Optional[InteractionFlags] = None):
237
        """|coro|
238
239
        Acknowledge an interaction, any flags here are applied to the reply.
240
241
        Parameters
242
        ----------
243
        flags: :class:`~pincer.objects.app.interaction_flags.InteractionFlags`
244
            The flags which must be applied to the reply.
245
246
        Raises
247
        ------
248
        :class:`~pincer.exceptions.InteractionAlreadyAcknowledged`
249
            The interaction was already acknowledged, this can be
250
            because a reply or ack was already sent.
251
        """
252
        if self.has_replied or self.has_acknowledged:
253
            raise InteractionAlreadyAcknowledged(
254
                "The interaction you are trying to acknowledge has already "
255
                "been acknowledged"
256
            )
257
258
        self.has_acknowledged = True
259
        await self._http.post(
260
            f"interactions/{self.id}/{self.token}/callback",
261
            {
262
                "type": CallbackType.DEFERRED_MESSAGE,
263
                "data": {
264
                    "flags": flags
265
                }
266
            }
267
        )
268
269
    async def __post_send_handler(self, message: Message):
270
        """Process the interaction after it was sent.
271
272
        Parameters
273
        ----------
274
        message :class:`~pincer.objects.message.message.Message`
275
            The interaction message.
276
        """
277
278
        if message.delete_after:
279
            await sleep(message.delete_after)
280
            await self.delete()
281
282
    def __post_sent(self, message: Message):
283
        """Ensure the `__post_send_handler` method its future.
284
285
        Parameters
286
        ----------
287
        message :class:`~pincer.objects.message.message.Message`
288
            The interaction message.
289
        """
290
        self.has_replied = True
291
        ensure_future(self.__post_send_handler(message))
292
293
    async def reply(self, message: MessageConvertable):
294
        """|coro|
295
296
        Initial reply, only works if no ACK has been sent yet.
297
298
        Parameters
299
        ----------
300
        message :class:`~pincer.utils.convert_message.MessageConvertable`
301
            The response message!
302
303
        Raises
304
        ------
305
        :class:`~.pincer.errors.UseFollowup`
306
            Exception raised when a reply has already been sent so a
307
            :func:`~pincer.objects.app.interactions.Interaction.followup`
308
            should be used instead.
309
        :class:`~.pincer.errors.InteractionTimedOut`
310
            Exception raised when discord had to wait too long for a reply.
311
            You can extend the discord wait time by using the
312
            :func:`~pincer.objects.app.interaction.Interaction.ack`
313
            function.
314
        """
315
        if self.has_replied:
0 ignored issues
show
Unused Code introduced by
Unnecessary "elif" after "raise"
Loading history...
316
            raise UseFollowup(
317
                "A response has already been sent to the interaction. "
318
                "Please use a followup instead!"
319
            )
320
        elif self.has_acknowledged:
321
            self.has_replied = True
322
            await self.edit(message)
323
            return
324
325
        message = convert_message(self._client, message)
326
        content_type, data = message.serialize(
327
            message_type=CallbackType.MESSAGE
328
        )
329
330
        try:
331
            await self._http.post(
332
                f"interactions/{self.id}/{self.token}/callback",
333
                data,
334
                content_type=content_type
335
            )
336
        except NotFoundError:
337
            raise InteractionTimedOut(
338
                "Discord had to wait too long for the interaction reply, "
339
                "you can extend the time it takes for discord to timeout by "
340
                "acknowledging the interaction. (using interaction.ack)"
341
            )
342
343
        self.__post_sent(message)
344
345
    async def edit(self, message: MessageConvertable) -> UserMessage:
346
        """|coro|
347
348
        Edit an interaction. This is also the way to reply to
349
        interactions whom have been acknowledged.
350
351
        Parameters
352
        ----------
353
        message :class:`~pincer.utils.convert_message.MessageConvertable`
354
            The new message!
355
356
        Returns
357
        -------
358
        :class:`~pincer.objects.message.user_message.UserMessage`
359
            The updated message object.
360
361
        Raises
362
        ------
363
        :class:`~.pincer.errors.InteractionDoesNotExist`
364
            Exception raised when no reply has been sent.
365
        """
366
367
        if not self.has_replied:
368
            raise InteractionDoesNotExist(
369
                "The interaction whom you are trying to edit has not "
370
                "been sent yet!"
371
            )
372
373
        message = convert_message(self._client, message)
374
        content_type, data = message.serialize()
375
376
        resp = await self._http.patch(
377
            f"webhooks/{self._client.bot.id}/{self.token}/messages/@original",
378
            data,
379
            content_type=content_type
380
        )
381
        self.__post_sent(message)
382
        return UserMessage.from_dict(resp)
383
384
    async def delete(self):
385
        """|coro|
386
387
        Delete the interaction.
388
389
        Raises
390
        ------
391
        :class:`~pincer.errors.InteractionDoesNotExist`
392
            Exception raised when no reply has been sent.
393
        """
394
        if not self.has_replied:
395
            raise InteractionDoesNotExist(
396
                "The interaction whom you are trying to delete has not "
397
                "been sent yet!"
398
            )
399
400
        await self._http.delete(
401
            f"webhooks/{self._client.bot.id}/{self.token}/messages/@original"
402
        )
403
404
    async def __post_followup_send_handler(
405
            self,
406
            followup: UserMessage,
407
            message: Message
408
    ):
409
        """Process a followup after it was sent.
410
411
        Parameters
412
        ----------
413
        followup :class:`~pincer.objects.message.user_message.UserMessage`
414
            The followup message that is being post processed.
415
        message :class:`~pincer.objects.message.message.Message`
416
            The followup message.
417
        """
418
419
        if message.delete_after:
420
            await sleep(message.delete_after)
421
            await self.delete_followup(followup.id)
422
423
    def __post_followup_sent(
424
            self,
425
            followup: UserMessage,
426
            message: Message
427
    ):
428
        """Ensure the `__post_followup_send_handler` method its future.
429
430
        Parameters
431
        ----------
432
        followup :class:`~pincer.objects.message.user_message.UserMessage`
433
            The followup message that is being post processed.
434
        message :class:`~pincer.objects.message.message.Message`
435
            The followup message.
436
        """
437
        ensure_future(self.__post_followup_send_handler(followup, message))
438
439
    async def followup(self, message: MessageConvertable) -> UserMessage:
440
        """|coro|
441
442
        Create a follow up message for the interaction.
443
        This allows you to respond with multiple messages.
444
445
        Parameters
446
        ----------
447
        message :class:`~pincer.utils.convert_message.MessageConvertable`
448
            The message to sent.
449
450
        Returns
451
        -------
452
        :class:`~pincer.objects.message.user_message.UserMessage`
453
            The message that has been sent.
454
        """
455
        message = convert_message(self._client, message)
456
        content_type, data = message.serialize()
457
458
        resp = await self._http.post(
459
            f"webhooks/{self._client.bot.id}/{self.token}",
460
            data,
461
            content_type=content_type
462
        )
463
        msg = UserMessage.from_dict(resp)
464
        self.__post_followup_sent(msg, message)
465
        return msg
466
467
    async def edit_followup(
468
            self,
469
            message_id: int,
470
            message: MessageConvertable
471
    ) -> UserMessage:
472
        """|coro|
473
474
        Edit a followup message.
475
476
        Parameters
477
        ----------
478
        message_id :class:`int`
479
            The id of the original followup message.
480
        message :class:`~pincer.utils.convert_message.MessageConvertable`
481
            The message new message.
482
483
        Returns
484
        -------
485
        :class:`~pincer.objects.message.user_message.UserMessage`
486
            The updated message object.
487
        """
488
        message = convert_message(self._client, message)
489
        content_type, data = message.serialize()
490
491
        resp = await self._http.patch(
492
            f"webhooks/{self._client.bot.id}/{self.token}/messages/{message_id}",
493
            data,
494
            content_type=content_type
495
        )
496
        msg = UserMessage.from_dict(resp)
497
        self.__post_followup_sent(msg, message)
498
        return msg
499
500
    async def get_followup(self, message_id: int) -> UserMessage:
501
        """|coro|
502
503
        Get a followup message by id.
504
505
        Parameters
506
        ----------
507
        message_id :class:`int`
508
            The id of the original followup message that must be fetched.
509
510
        Returns
511
        -------
512
        :class:`~pincer.objects.message.user_message.UserMessage`
513
            The fetched message object.
514
        """
515
516
        resp = await self._http.get(
517
            f"webhooks/{self._client.bot.id}/{self.token}/messages/{message_id}",
518
        )
519
        return UserMessage.from_dict(resp)
520
521
    async def delete_followup(self, message: Union[UserMessage, int]):
522
        """|coro|
523
524
        Remove a followup message by id.
525
526
        Parameters
527
        ----------
528
        message Union[:class:`~pincer.objects.user_message.UserMessage`, :class:`int`]
529
            The id/followup object of the followup message that must be deleted.
530
        """
531
        message_id = message if isinstance(message, int) else message.id
532
533
        await self._http.delete(
534
            f"webhooks/{self._client.bot.id}/{self.token}/messages/{message_id}",
535
        )
536