Passed
Pull Request — main (#321)
by
unknown
02:02
created

Interaction.__post_init__()   D

Complexity

Conditions 12

Size

Total Lines 44
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 33
dl 0
loc 44
rs 4.8
c 0
b 0
f 0
cc 12
nop 1

How to fix   Complexity   

Complexity

Complex classes like pincer.objects.app.interactions.Interaction.__post_init__() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 pincer.utils.api_object import ChannelProperty, GuildProperty
11
12
from .command_types import AppCommandOptionType
13
from .interaction_base import InteractionType, CallbackType
14
from .mentionable import Mentionable
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(repr=False)
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(repr=False)
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
    """  # noqa: E501
86
    id: APINullable[Snowflake] = MISSING
87
    name: APINullable[str] = MISSING
88
    type: APINullable[int] = MISSING
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[List[str]] = MISSING
94
    target_id: APINullable[Snowflake] = MISSING
95
96
97
@dataclass(repr=False)
98
class Interaction(APIObject, ChannelProperty, GuildProperty):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (13/7)
Loading history...
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
        if not self.data.options:
146
            return
147
148
        for option in self.data.options:
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
    @staticmethod
190
    def return_type(
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
    ) -> Optional[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
        if data:
203
            return data[option.value]
204
205
        return None
206
207
    def get_message_context(self):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
208
        return MessageContext(
209
            self._client,
210
            self.member or self.user,
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 _base_ack(
237
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
238
        flags: Optional[InteractionFlags],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
239
        callback_type: CallbackType
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
240
    ):
241
        """|coro|
242
243
        Acknowledge an interaction, any flags here are applied to the reply.
244
245
        Parameters
246
        ----------
247
        message_type : :class:`~pincer.objects.app.interaction_base.CallbackType`
248
            The type of the message. Should be ``DEFERRED_MESSAGE`` or
249
            ``DEFERRED_UPDATE_MESSAGE``.
250
        flags: :class:`~pincer.objects.app.interaction_flags.InteractionFlags`
251
            The flags which must be applied to the reply.
252
253
        Raises
254
        ------
255
        :class:`~pincer.exceptions.InteractionAlreadyAcknowledged`
256
            The interaction was already acknowledged, this can be
257
            because a reply or ack was already sent.
258
        """
259
        if self.has_replied or self.has_acknowledged:
260
            raise InteractionAlreadyAcknowledged(
261
                "The interaction you are trying to acknowledge has already "
262
                "been acknowledged"
263
            )
264
265
        self.has_acknowledged = True
266
        await self._http.post(
267
            f"interactions/{self.id}/{self.token}/callback",
268
            {
269
                "type": callback_type,
270
                "data": {
271
                    "flags": flags
272
                }
273
            }
274
        )
275
276
    async def ack(self, flags: Optional[InteractionFlags] = None):
277
        """|coro|
278
279
        Acknowledge an interaction, any flags here are applied to the reply.
280
281
        Parameters
282
        ----------
283
        flags :class:`~pincer.objects.app.interaction_flags.InteractionFlags`
284
            The flags which must be applied to the reply. |default| :data:`None`
285
        """
286
        return await self._base_ack(flags, CallbackType.DEFERRED_MESSAGE)
287
288
    async def deferred_update_ack(self, flags: Optional[InteractionFlags] = None):
289
        """|coro|
290
        Same as ack but for updating a message.
291
292
        Parameters
293
        ----------
294
        flags :class:`~pincer.objects.app.interaction_flags.InteractionFlags`
295
            The flags which must be applied to the reply. |default| :data:`None`
296
        """
297
        return await self._base_ack(flags, CallbackType.DEFERRED_UPDATE_MESSAGE)
298
299
    async def __post_send_handler(self, message: Message):
300
        """Process the interaction after it was sent.
301
302
        Parameters
303
        ----------
304
        message :class:`~pincer.objects.message.message.Message`
305
            The interaction message.
306
        """
307
308
        if message.delete_after:
309
            await sleep(message.delete_after)
310
            await self.delete()
311
312
    def __post_sent(self, message: Message):
313
        """Ensure the `__post_send_handler` method its future.
314
315
        Parameters
316
        ----------
317
        message :class:`~pincer.objects.message.message.Message`
318
            The interaction message.
319
        """
320
        self.has_replied = True
321
        ensure_future(self.__post_send_handler(message))
322
323
    async def _base_reply(
324
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
325
        message: MessageConvertable,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
326
        message_type: CallbackType,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
327
        allow_empty: bool
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
328
    ):
329
        """|coro|
330
331
        Initial reply, only works if no ACK has been sent yet.
332
333
        Parameters
334
        ----------
335
        message :class:`~pincer.utils.convert_message.MessageConvertable`
336
            The response message!
337
        message_type : :class:`~pincer.objects.app.interaction_base.CallbackType`
338
            The type of the message. Should be ``MESSAGE`` or
339
            ``UPDATE_MESSAGE``.
340
341
        Raises
342
        ------
343
        :class:`~.pincer.errors.UseFollowup`
344
            Exception raised when a reply has already been sent so a
345
            :func:`~pincer.objects.app.interactions.Interaction.followup`
346
            should be used instead.
347
        :class:`~.pincer.errors.InteractionTimedOut`
348
            Exception raised when discord had to wait too long for a reply.
349
            You can extend the discord wait time by using the
350
            :func:`~pincer.objects.app.interaction.Interaction.ack`
351
            function.
352
        """
353
        if self.has_replied:
0 ignored issues
show
Unused Code introduced by
Unnecessary "elif" after "raise"
Loading history...
354
            raise UseFollowup(
355
                "A response has already been sent to the interaction. "
356
                "Please use a followup instead!"
357
            )
358
        elif self.has_acknowledged:
359
            self.has_replied = True
360
            await self.edit(message)
361
            return
362
363
        message = convert_message(self._client, message)
364
        content_type, data = message.serialize(
365
            message_type=message_type,
366
            allow_empty=allow_empty
367
        )
368
369
        try:
370
            await self._http.post(
371
                f"interactions/{self.id}/{self.token}/callback",
372
                data,
373
                content_type=content_type
374
            )
375
        except NotFoundError:
376
            raise InteractionTimedOut(
377
                "Discord had to wait too long for the interaction reply, "
378
                "you can extend the time it takes for discord to timeout by "
379
                "acknowledging the interaction. (using interaction.ack)"
380
            )
381
382
        self.__post_sent(message)
383
384
    async def reply(self, message: MessageConvertable) -> UserMessage:
385
        """|coro|
386
        Sends a reply to a interaction.
387
        """
388
        return await self._base_reply(message, CallbackType.MESSAGE, False)
389
390
    async def update(self, message: MessageConvertable) -> UserMessage:
391
        """|coro|
392
        Edits the reply to an interaction. Only works with Message Component
393
        Interactions.
394
        """
395
        return await self._base_reply(message, CallbackType.UPDATE_MESSAGE, True)
396
397
    async def edit(self, message: MessageConvertable) -> UserMessage:
398
        """|coro|
399
400
        Edit an interaction. This is also the way to reply to
401
        interactions whom have been acknowledged.
402
403
        Parameters
404
        ----------
405
        message :class:`~pincer.utils.convert_message.MessageConvertable`
406
            The new message!
407
408
        Returns
409
        -------
410
        :class:`~pincer.objects.message.user_message.UserMessage`
411
            The updated message object.
412
413
        Raises
414
        ------
415
        :class:`~.pincer.errors.InteractionDoesNotExist`
416
            Exception raised when no reply has been sent.
417
        """
418
419
        if not self.has_replied:
420
            raise InteractionDoesNotExist(
421
                "The interaction whom you are trying to edit has not "
422
                "been sent yet!"
423
            )
424
425
        message = convert_message(self._client, message)
426
        content_type, data = message.serialize()
427
428
        resp = await self._http.patch(
429
            f"webhooks/{self._client.bot.id}/{self.token}/messages/@original",
430
            data,
431
            content_type=content_type
432
        )
433
        self.__post_sent(message)
434
        return UserMessage.from_dict(resp)
435
436
    async def delete(self):
437
        """|coro|
438
439
        Delete the interaction.
440
441
        Raises
442
        ------
443
        :class:`~pincer.errors.InteractionDoesNotExist`
444
            Exception raised when no reply has been sent.
445
        """
446
        if not self.has_replied:
447
            raise InteractionDoesNotExist(
448
                "The interaction whom you are trying to delete has not "
449
                "been sent yet!"
450
            )
451
452
        await self._http.delete(
453
            f"webhooks/{self._client.bot.id}/{self.token}/messages/@original"
454
        )
455
456
    async def __post_followup_send_handler(
457
            self,
458
            followup: UserMessage,
459
            message: Message
460
    ):
461
        """Process a followup after it was sent.
462
463
        Parameters
464
        ----------
465
        followup :class:`~pincer.objects.message.user_message.UserMessage`
466
            The followup message that is being post processed.
467
        message :class:`~pincer.objects.message.message.Message`
468
            The followup message.
469
        """
470
471
        if message.delete_after:
472
            await sleep(message.delete_after)
473
            await self.delete_followup(followup.id)
474
475
    def __post_followup_sent(
476
            self,
477
            followup: UserMessage,
478
            message: Message
479
    ):
480
        """Ensure the `__post_followup_send_handler` method its future.
481
482
        Parameters
483
        ----------
484
        followup :class:`~pincer.objects.message.user_message.UserMessage`
485
            The followup message that is being post processed.
486
        message :class:`~pincer.objects.message.message.Message`
487
            The followup message.
488
        """
489
        ensure_future(self.__post_followup_send_handler(followup, message))
490
491
    async def followup(self, message: MessageConvertable) -> UserMessage:
492
        """|coro|
493
494
        Create a follow-up message for the interaction.
495
        This allows you to respond with multiple messages.
496
497
        Parameters
498
        ----------
499
        message :class:`~pincer.utils.convert_message.MessageConvertable`
500
            The message to send.
501
502
        Returns
503
        -------
504
        :class:`~pincer.objects.message.user_message.UserMessage`
505
            The message that has been sent.
506
        """
507
        message = convert_message(self._client, message)
508
        content_type, data = message.serialize()
509
510
        resp = await self._http.post(
511
            f"webhooks/{self._client.bot.id}/{self.token}",
512
            data,
513
            content_type=content_type
514
        )
515
        msg = UserMessage.from_dict(resp)
516
        self.__post_followup_sent(msg, message)
517
        return msg
518
519
    async def edit_followup(
520
            self,
521
            message_id: int,
522
            message: MessageConvertable
523
    ) -> UserMessage:
524
        """|coro|
525
526
        Edit a followup message.
527
528
        Parameters
529
        ----------
530
        message_id :class:`int`
531
            The id of the original followup message.
532
        message :class:`~pincer.utils.convert_message.MessageConvertable`
533
            The message new message.
534
535
        Returns
536
        -------
537
        :class:`~pincer.objects.message.user_message.UserMessage`
538
            The updated message object.
539
        """
540
        message = convert_message(self._client, message)
541
        content_type, data = message.serialize()
542
543
        resp = await self._http.patch(
544
            f"webhooks/{self._client.bot.id}/{self.token}/messages/{message_id}",
545
            data,
546
            content_type=content_type
547
        )
548
        msg = UserMessage.from_dict(resp)
549
        self.__post_followup_sent(msg, message)
550
        return msg
551
552
    async def get_followup(self, message_id: int) -> UserMessage:
553
        """|coro|
554
555
        Get a followup message by id.
556
557
        Parameters
558
        ----------
559
        message_id :class:`int`
560
            The id of the original followup message that must be fetched.
561
562
        Returns
563
        -------
564
        :class:`~pincer.objects.message.user_message.UserMessage`
565
            The fetched message object.
566
        """
567
568
        resp = await self._http.get(
569
            f"webhooks/{self._client.bot.id}/{self.token}/messages/{message_id}",
570
        )
571
        return UserMessage.from_dict(resp)
572
573
    async def delete_followup(self, message: Union[UserMessage, int]):
574
        """|coro|
575
576
        Remove a followup message by id.
577
578
        Parameters
579
        ----------
580
        message Union[:class:`~pincer.objects.user_message.UserMessage`, :class:`int`]
581
            The id/followup object of the followup message that must be deleted.
582
        """
583
        message_id = message if isinstance(message, int) else message.id
584
585
        await self._http.delete(
586
            f"webhooks/{self._client.bot.id}/{self.token}/messages/{message_id}",
587
        )
588