Passed
Pull Request — main (#218)
by
unknown
01:58
created

Interaction.__post_init__()   A

Complexity

Conditions 4

Size

Total Lines 45
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

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