From 5b78097cef292ba394b3f95c5157d2251b74ae78 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 18 Feb 2025 08:10:59 +0100 Subject: [PATCH] Add support for Interaction Callback Resource Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/interactions.py | 213 ++++++++++++++++++++++++++++++++-- discord/types/interactions.py | 34 ++++++ discord/webhook/async_.py | 16 ++- docs/interactions/api.rst | 16 +++ 4 files changed, 269 insertions(+), 10 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 49bfbfb07203..07c600a5dde0 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -54,6 +54,8 @@ 'Interaction', 'InteractionMessage', 'InteractionResponse', + 'InteractionCallback', + 'InteractionCallbackActivityInstance', ) if TYPE_CHECKING: @@ -61,6 +63,8 @@ Interaction as InteractionPayload, InteractionData, ApplicationCommandInteractionData, + InteractionCallback as InteractionCallbackPayload, + InteractionCallbackActivity as InteractionCallbackActivityPayload, ) from .types.webhook import ( Webhook as WebhookPayload, @@ -90,6 +94,10 @@ DMChannel, GroupChannel, ] + InteractionCallbackResource = Union[ + "InteractionMessage", + "InteractionCallbackActivityInstance", + ] MISSING: Any = utils.MISSING @@ -469,6 +477,7 @@ async def edit_original_response( attachments: Sequence[Union[Attachment, File]] = MISSING, view: Optional[View] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, + poll: Poll = MISSING, ) -> InteractionMessage: """|coro| @@ -503,6 +512,14 @@ async def edit_original_response( view: Optional[:class:`~discord.ui.View`] The updated view to update this message with. If ``None`` is passed then the view is removed. + poll: :class:`Poll` + The poll to create when editing the message. + + .. versionadded:: 2.5 + + .. note:: + + This is only accepted when the response type is :attr:`InteractionResponseType.deferred_channel_message`. Raises ------- @@ -532,6 +549,7 @@ async def edit_original_response( view=view, allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, + poll=poll, ) as params: adapter = async_context.get() http = self._state.http @@ -624,6 +642,106 @@ async def translate( return await translator.translate(string, locale=locale, context=context) +class InteractionCallbackActivityInstance: + """Represents an activity instance launched as an interaction response. + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`str` + The activity instance ID. + """ + + __slots__ = ('id',) + + def __init__(self, data: InteractionCallbackActivityPayload) -> None: + self.id: str = data['id'] + + +class InteractionCallback(Generic[ClientT]): + """Represents an interaction response callback. + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`int` + The interaction ID. + type: :class:`InteractionResponseType` + The interaction callback response type. + resource: Optional[Union[:class:`InteractionMessage`, :class:`InteractionCallbackActivityInstance`]] + The resource that the interaction response created. If a message was sent, this will be + a :class:`InteractionMessage`. If an activity was launched this will be a + :class:`InteractionCallbackActivityInstance`. In any other case, this will be ``None``. + message_id: Optional[:class:`int`] + The message ID of the resource. Only available if the resource is a :class:`InteractionMessage`. + activity_id: Optional[:class:`str`] + The activity ID of the resource. Only available if the resource is a :class:`InteractionCallbackActivityInstance`. + """ + + __slots__ = ( + '_state', + '_parent', + 'type', + 'id', + '_thinking', + '_ephemeral', + 'message_id', + 'activity_id', + 'resource', + ) + + def __init__( + self, + *, + data: InteractionCallbackPayload, + parent: Interaction[ClientT], + state: ConnectionState, + type: InteractionResponseType, + ) -> None: + self._state: ConnectionState = state + self._parent: Interaction[ClientT] = parent + self.type: InteractionResponseType = type + self._update(data) + + def _update(self, data: InteractionCallbackPayload) -> None: + interaction = data['interaction'] + + self.id: int = int(interaction['id']) + self._thinking: bool = interaction.get('response_message_loading', False) + self._ephemeral: bool = interaction.get('response_message_ephemeral', False) + + self.message_id: Optional[int] = utils._get_as_snowflake(interaction, 'response_message_id') + self.activity_id: Optional[str] = interaction.get('activity_instance_id') + + self.resource: Optional[InteractionCallbackResource] = None + + resource = data.get('resource') + if resource is not None: + + self.type = try_enum(InteractionResponseType, resource['type']) + + message = resource.get('message') + activity_instance = resource.get('activity_instance') + if message is not None: + self.resource = InteractionMessage( + state=self._state, + channel=self._parent.channel, # type: ignore # channel should be the correct type here + data=message, + ) + elif activity_instance is not None: + self.resource = InteractionCallbackActivityInstance(activity_instance) + + def is_thinking(self) -> bool: + """:class:`bool`: Whether the response was a thinking defer.""" + return self._thinking + + def is_ephemeral(self) -> bool: + """:class:`bool`: Whether the response was ephemeral.""" + return self._ephemeral + + class InteractionResponse(Generic[ClientT]): """Represents a Discord interaction response. @@ -653,7 +771,12 @@ def type(self) -> Optional[InteractionResponseType]: """:class:`InteractionResponseType`: The type of response that was sent, ``None`` if response is not done.""" return self._response_type - async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> None: + async def defer( + self, + *, + ephemeral: bool = False, + thinking: bool = False, + ) -> Optional[InteractionCallback[ClientT]]: """|coro| Defers the interaction response. @@ -667,6 +790,9 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non - :attr:`InteractionType.component` - :attr:`InteractionType.modal_submit` + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallback` instance. + Parameters ----------- ephemeral: :class:`bool` @@ -685,6 +811,11 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non Deferring the interaction failed. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + Optional[:class:`InteractionCallback`] + The interaction callback resource, or ``None``. """ if self._response_type: raise InteractionResponded(self._parent) @@ -709,7 +840,7 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non adapter = async_context.get() params = interaction_response_params(type=defer_type, data=data) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -718,6 +849,12 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non params=params, ) self._response_type = InteractionResponseType(defer_type) + return InteractionCallback( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) async def pong(self) -> None: """|coro| @@ -767,11 +904,14 @@ async def send_message( silent: bool = False, delete_after: Optional[float] = None, poll: Poll = MISSING, - ) -> None: + ) -> InteractionCallback[ClientT]: """|coro| Responds to this interaction by sending a message. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallback` instance. + Parameters ----------- content: Optional[:class:`str`] @@ -825,6 +965,11 @@ async def send_message( The length of ``embeds`` was invalid. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + :class:`InteractionCallback` + The interaction callback data. """ if self._response_type: raise InteractionResponded(self._parent) @@ -855,7 +1000,7 @@ async def send_message( ) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -886,6 +1031,13 @@ async def inner_call(delay: float = delete_after): asyncio.create_task(inner_call()) + return InteractionCallback( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + async def edit_message( self, *, @@ -897,12 +1049,15 @@ async def edit_message( allowed_mentions: Optional[AllowedMentions] = MISSING, delete_after: Optional[float] = None, suppress_embeds: bool = MISSING, - ) -> None: + ) -> Optional[InteractionCallback[ClientT]]: """|coro| Responds to this interaction by editing the original message of a component or modal interaction. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallback` instance. + Parameters ----------- content: Optional[:class:`str`] @@ -948,6 +1103,11 @@ async def edit_message( You specified both ``embed`` and ``embeds``. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + Optional[:class:`InteractionCallback`] + The interaction callback data, or ``None`` if editing the message was not possible. """ if self._response_type: raise InteractionResponded(self._parent) @@ -990,7 +1150,7 @@ async def edit_message( ) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -1015,15 +1175,29 @@ async def inner_call(delay: float = delete_after): asyncio.create_task(inner_call()) - async def send_modal(self, modal: Modal, /) -> None: + return InteractionCallback( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + + async def send_modal(self, modal: Modal, /) -> InteractionCallback[ClientT]: """|coro| Responds to this interaction by sending a modal. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallback` instance. + Parameters ----------- modal: :class:`~discord.ui.Modal` The modal to send. + with_response: :class:`bool` + Whether to return the interaction response callback resource. + + .. versionadded:: 2.5 Raises ------- @@ -1031,6 +1205,11 @@ async def send_modal(self, modal: Modal, /) -> None: Sending the modal failed. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + :class:`InteractionCallback` + The interaction callback data. """ if self._response_type: raise InteractionResponded(self._parent) @@ -1041,7 +1220,7 @@ async def send_modal(self, modal: Modal, /) -> None: http = parent._state.http params = interaction_response_params(InteractionResponseType.modal.value, modal.to_dict()) - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -1053,6 +1232,13 @@ async def send_modal(self, modal: Modal, /) -> None: self._parent._state.store_view(modal) self._response_type = InteractionResponseType.modal + return InteractionCallback( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + async def autocomplete(self, choices: Sequence[Choice[ChoiceT]]) -> None: """|coro| @@ -1154,6 +1340,7 @@ async def edit( view: Optional[View] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, delete_after: Optional[float] = None, + poll: Poll = MISSING, ) -> InteractionMessage: """|coro| @@ -1188,6 +1375,15 @@ async def edit( then it is silently ignored. .. versionadded:: 2.2 + poll: :class:`~discord.Poll` + The poll to create when editing the message. + + .. versionadded:: 2.5 + + .. note:: + + This is only accepted if the interaction response's :attr:`InteractionResponse.type` + attribute is :attr:`InteractionResponseType.deferred_channel_message`. Raises ------- @@ -1212,6 +1408,7 @@ async def edit( attachments=attachments, view=view, allowed_mentions=allowed_mentions, + poll=poll, ) if delete_after is not None: await self.delete(delay=delete_after) diff --git a/discord/types/interactions.py b/discord/types/interactions.py index a72a5b2cea15..3f3516c3a696 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -42,6 +42,16 @@ InteractionType = Literal[1, 2, 3, 4, 5] +InteractionResponseType = Literal[ + 1, + 4, + 5, + 6, + 7, + 8, + 9, + 10, +] InteractionContextType = Literal[0, 1, 2] InteractionInstallationType = Literal[0, 1] @@ -301,3 +311,27 @@ class ModalSubmitMessageInteractionMetadata(_MessageInteractionMetadata): MessageComponentMessageInteractionMetadata, ModalSubmitMessageInteractionMetadata, ] + + +class InteractionCallbackResponse(TypedDict): + id: Snowflake + type: InteractionType + activity_instance_id: NotRequired[str] + response_message_id: NotRequired[Snowflake] + response_message_loading: NotRequired[bool] + response_message_ephemeral: NotRequired[bool] + + +class InteractionCallbackActivity(TypedDict): + id: str + + +class InteractionCallbackResource(TypedDict): + type: InteractionResponseType + activity_instance: NotRequired[InteractionCallbackActivity] + message: NotRequired[Message] + + +class InteractionCallback(TypedDict): + interaction: InteractionCallbackResponse + resource: NotRequired[InteractionCallbackResource] diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 1966832e0f0a..3b62b10faa2c 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -90,6 +90,9 @@ ) from ..types.emoji import PartialEmoji as PartialEmojiPayload from ..types.snowflake import SnowflakeList + from ..types.interactions import ( + InteractionCallback as InteractionCallbackResponsePayload, + ) BE = TypeVar('BE', bound=BaseException) _State = Union[ConnectionState, '_WebhookState'] @@ -435,13 +438,14 @@ def create_interaction_response( proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, params: MultipartParameters, - ) -> Response[None]: + ) -> Response[InteractionCallbackResponsePayload]: route = Route( 'POST', '/interactions/{webhook_id}/{webhook_token}/callback', webhook_id=interaction_id, webhook_token=token, ) + request_params = {'with_response': '1'} if params.files: return self.request( @@ -451,9 +455,17 @@ def create_interaction_response( proxy_auth=proxy_auth, files=params.files, multipart=params.multipart, + params=request_params, ) else: - return self.request(route, session=session, proxy=proxy, proxy_auth=proxy_auth, payload=params.payload) + return self.request( + route, + session=session, + proxy=proxy, + proxy_auth=proxy_auth, + payload=params.payload, + params=request_params, + ) def get_original_interaction_response( self, diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index aeb6a25c613d..0bf69903bfc7 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -28,6 +28,22 @@ InteractionResponse .. autoclass:: InteractionResponse() :members: +InteractionCallback +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionCallback + +.. autoclass:: InteractionCallback() + :members: + +InteractionCallbackActivityInstance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionCallbackActivityInstance + +.. autoclass:: InteractionCallbackActivityInstance() + :members: + InteractionMessage ~~~~~~~~~~~~~~~~~~~