From 6b0b510818861500255e16260a2159ceac677ca0 Mon Sep 17 00:00:00 2001 From: dolfies Date: Sat, 13 Nov 2021 19:58:17 -0500 Subject: [PATCH] Handle interaction events --- discord/components.py | 51 ++++++++++++++++++++++++++++++++++++++++++- discord/message.py | 2 +- discord/state.py | 23 +++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index 116b80c88..656cb175c 100644 --- a/discord/components.py +++ b/discord/components.py @@ -24,10 +24,12 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations +from asyncio import TimeoutError from datetime import datetime from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union from .enums import try_enum, ComponentType, ButtonStyle +from .errors import InvalidData from .utils import get_slots, MISSING, time_snowflake from .partial_emoji import PartialEmoji, _EmojiTag @@ -54,6 +56,32 @@ __all__ = ( C = TypeVar('C', bound='Component') +class Interaction: + """Represents an interaction. + + Attributes + ------------ + id: :class:`int` + The interaction ID. + nonce: Optional[Union[:class:`int`, :class:`str`]] + The interaction's nonce. + successful: Optional[:class:`bool`] + Whether the interaction succeeded. + This is not immediately available, and is filled when Discord notifies us about the outcome of the interaction. + """ + + __slots__ = ('id', 'nonce', 'successful') + + def __init__(self, *, id: int, nonce: Optional[Union[int, str]] = None) -> None: + self.id = id + self.nonce = nonce + self.successful: Optional[bool] = None + + def __repr__(self) -> str: + s = self.successful + return f'' + + class Component: """Represents a Discord Bot UI Kit Component. @@ -210,10 +238,21 @@ class Button(Component): Raises ------- + InvalidData + Didn't receive a response from Discord. + Doesn't mean the interaction failed. + NotFound + The originating message was not found. HTTPException Clicking the button failed. + + Returns + -------- + :class:`Interaction` + The interaction that was created. """ message = self.message + state = message._state payload = { 'application_id': str(message.application_id), 'channel_id': str(message.channel.id), @@ -227,7 +266,17 @@ class Button(Component): 'nonce': str(time_snowflake(datetime.utcnow())), 'type': 3, # Should be an enum but eh } - await message._state.http.interact(payload) # type: ignore + await state.http.interact(payload) + + try: + i = await state.client.wait_for( + 'interaction', + check=lambda d: d.nonce == payload['nonce'], + timeout=5, + ) + except TimeoutError as exc: + raise InvalidData('Did not receive a response from Discord.') from exc + return i class SelectMenu(Component): diff --git a/discord/message.py b/discord/message.py index 635cd7104..615d37522 100644 --- a/discord/message.py +++ b/discord/message.py @@ -518,7 +518,7 @@ class Message(Hashable): content: :class:`str` The actual contents of the message. nonce: Optional[Union[:class:`str`, :class:`int`]] - The value used by the discord guild and the client to verify that the message is successfully sent. + The value used by Discord clients to verify that the message is successfully sent. This is not stored long term within Discord's servers and is only used ephemerally. embeds: List[:class:`Embed`] A list of embeds the message has. diff --git a/discord/state.py b/discord/state.py index 164dc3af5..a3093ae17 100644 --- a/discord/state.py +++ b/discord/state.py @@ -59,6 +59,7 @@ from .threads import Thread, ThreadMember from .sticker import GuildSticker from .settings import UserSettings from .tracking import Tracking +from .components import Interaction if TYPE_CHECKING: @@ -262,6 +263,7 @@ class ConnectionState: self._voice_clients: Dict[int, VoiceProtocol] = {} self._voice_states: Dict[int, VoiceState] = {} + self._interactions: Dict[int, Interaction] = {} self._relationships: Dict[int, Relationship] = {} self._private_channels: Dict[int, PrivateChannel] = {} self._private_channels_by_user: Dict[int, DMChannel] = {} @@ -1609,6 +1611,27 @@ class ConnectionState: else: self.dispatch('relationship_remove', old) + def parse_interaction_create(self, data) -> None: + i = Interaction(**data) + self._interactions[i.id] = i + self.dispatch('interaction', i) + + def parse_interaction_success(self, data) -> None: + id = int(data['id']) + i = self._interactions.pop(id, None) + if i is None: + i = Interaction(**data) + i.successful = True + self.dispatch('interaction_finish', i) + + def parse_interaction_failed(self, data) -> None: + id = int(data['id']) + i = self._interactions.pop(id, None) + if i is None: + i = Interaction(**data) + i.successful = False + self.dispatch('interaction_finish', i) + def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]: if isinstance(channel, TextChannel): return channel.guild.get_member(user_id)