Passed
Pull Request — main (#167)
by
unknown
01:38
created

pincer.objects.app.interactions   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 559
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 32
eloc 222
dl 0
loc 559
rs 9.84
c 0
b 0
f 0

16 Methods

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