Passed
Push — main ( e8771c...072bf6 )
by
unknown
01:47
created

pincer.objects.app.interactions   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 536
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 39
eloc 213
dl 0
loc 536
rs 9.28
c 0
b 0
f 0

16 Methods

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