diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d996a6d2..00b6422e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,9 @@ These changes are available on the `master` branch, but have not yet been releas ([#2714](https://github.com/Pycord-Development/pycord/pull/2714)) - Added the ability to pass a `datetime.time` object to `format_dt` ([#2747](https://github.com/Pycord-Development/pycord/pull/2747)) +- Implemented `with_response` for interaction callbacks, adding + `Interaction.callback.is_loading()` and `Interaction.callback.is_ephemeral()`. + ([#2711](https://github.com/Pycord-Development/pycord/pull/2711)) ### Fixed diff --git a/discord/interactions.py b/discord/interactions.py index 57628f4691..fbb2542e39 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -60,6 +60,7 @@ "MessageInteraction", "InteractionMetadata", "AuthorizingIntegrationOwners", + "InteractionCallback", ) if TYPE_CHECKING: @@ -82,7 +83,8 @@ from .state import ConnectionState from .threads import Thread from .types.interactions import Interaction as InteractionPayload - from .types.interactions import InteractionData + from .types.interactions import InteractionCallback as InteractionCallbackPayload + from .types.interactions import InteractionCallbackResponse, InteractionData from .types.interactions import InteractionMetadata as InteractionMetadataPayload from .types.interactions import MessageInteraction as MessageInteractionPayload from .ui.modal import Modal @@ -152,6 +154,11 @@ class Interaction: The context in which this command was executed. .. versionadded:: 2.6 + callback: Optional[:class:`InteractionCallback`] + The callback of the interaction. Contains information about the status of the interaction response. + Will be `None` until the interaction is responded to. + + .. versionadded:: 2.7 """ __slots__: tuple[str, ...] = ( @@ -172,6 +179,7 @@ class Interaction: "entitlements", "context", "authorizing_integration_owners", + "callback", "_channel_data", "_message_data", "_guild_data", @@ -191,6 +199,7 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState): self._state: ConnectionState = state self._session: ClientSession = state.http._HTTPClient__session self._original_response: InteractionMessage | None = None + self.callback: InteractionCallback | None = None self._from_data(data) def _from_data(self, data: InteractionPayload): @@ -825,18 +834,21 @@ async def defer(self, *, ephemeral: bool = False, invisible: bool = True) -> Non if defer_type: adapter = async_context.get() http = parent._state.http - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=defer_type, - data=data, - proxy=http.proxy, - proxy_auth=http.proxy_auth, + callback_response: InteractionCallbackResponse = ( + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=defer_type, + data=data, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + ) ) ) self._responded = True + await self._process_callback_response(callback_response) async def pong(self) -> None: """|coro| @@ -859,17 +871,36 @@ async def pong(self) -> None: if parent.type is InteractionType.ping: adapter = async_context.get() http = parent._state.http - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - type=InteractionResponseType.pong.value, + callback_response: InteractionCallbackResponse = ( + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + type=InteractionResponseType.pong.value, + ) ) ) self._responded = True + await self._process_callback_response(callback_response) + + async def _process_callback_response( + self, callback_response: InteractionCallbackResponse + ): + if callback_response.get("resource") and callback_response["resource"].get( + "message" + ): + # TODO: fix later to not raise? + channel = self._parent.channel + if channel is None: + raise ClientException("Channel for message could not be resolved") + state = _InteractionMessageState(self._parent, self._parent._state) + message = InteractionMessage(state=state, channel=channel, data=callback_response["resource"]["message"]) # type: ignore + self._parent._original_response = message + + self._parent.callback = InteractionCallback(callback_response["interaction"]) async def send_message( self, @@ -1007,16 +1038,18 @@ async def send_message( adapter = async_context.get() http = parent._state.http try: - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.channel_message.value, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - data=payload, - files=files, + callback_response: InteractionCallbackResponse = ( + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.channel_message.value, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + data=payload, + files=files, + ) ) ) finally: @@ -1032,6 +1065,7 @@ async def send_message( self._parent._state.store_view(view) self._responded = True + await self._process_callback_response(callback_response) if delete_after is not None: await self._parent.delete_original_response(delay=delete_after) return self._parent @@ -1171,16 +1205,18 @@ async def edit_message( adapter = async_context.get() http = parent._state.http try: - await self._locked_response( - adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.message_update.value, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - data=payload, - files=files, + callback_response: InteractionCallbackResponse = ( + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.message_update.value, + proxy=http.proxy, + proxy_auth=http.proxy_auth, + data=payload, + files=files, + ) ) ) finally: @@ -1193,6 +1229,7 @@ async def edit_message( state.store_view(view, message_id) self._responded = True + await self._process_callback_response(callback_response) if delete_after is not None: await self._parent.delete_original_response(delay=delete_after) @@ -1228,7 +1265,7 @@ async def send_autocomplete_result( adapter = async_context.get() http = parent._state.http - await self._locked_response( + callback_response: InteractionCallbackResponse = await self._locked_response( adapter.create_interaction_response( parent.id, parent.token, @@ -1241,6 +1278,7 @@ async def send_autocomplete_result( ) self._responded = True + await self._process_callback_response(callback_response) async def send_modal(self, modal: Modal) -> Interaction: """|coro| @@ -1267,7 +1305,7 @@ async def send_modal(self, modal: Modal) -> Interaction: payload = modal.to_dict() adapter = async_context.get() http = parent._state.http - await self._locked_response( + callback_response: InteractionCallbackResponse = await self._locked_response( adapter.create_interaction_response( parent.id, parent.token, @@ -1279,6 +1317,7 @@ async def send_modal(self, modal: Modal) -> Interaction: ) ) self._responded = True + await self._process_callback_response(callback_response) self._parent._state.store_modal(modal, self._parent.user.id) return self._parent @@ -1306,7 +1345,7 @@ async def premium_required(self) -> Interaction: adapter = async_context.get() http = parent._state.http - await self._locked_response( + callback_response: InteractionCallbackResponse = await self._locked_response( adapter.create_interaction_response( parent.id, parent.token, @@ -1317,9 +1356,10 @@ async def premium_required(self) -> Interaction: ) ) self._responded = True + await self._process_callback_response(callback_response) return self._parent - async def _locked_response(self, coro: Coroutine[Any, Any, Any]) -> None: + async def _locked_response(self, coro: Coroutine[Any, Any, Any]) -> Any: """|coro| Wraps a response and makes sure that it's locked while executing. @@ -1338,7 +1378,7 @@ async def _locked_response(self, coro: Coroutine[Any, Any, Any]) -> None: if self.is_done(): coro.close() # cleanup un-awaited coroutine raise InteractionResponded(self._parent) - await coro + return await coro class _InteractionMessageState: @@ -1662,3 +1702,29 @@ def guild(self) -> Guild | None: if not self.guild_id: return None return self._state._get_guild(self.guild_id) + + +class InteractionCallback: + """Information about the status of the interaction response. + + .. versionadded:: 2.7 + """ + + def __init__(self, data: InteractionCallbackPayload): + self._response_message_loading: bool = data.get( + "response_message_loading", False + ) + self._response_message_ephemeral: bool = data.get( + "response_message_ephemeral", False + ) + + def is_loading(self) -> bool: + """Indicates whether the response message is in a loading state.""" + return self._response_message_loading + + def is_ephemeral(self) -> bool: + """Indicates whether the response message is ephemeral. + + This might be useful for determining if the message was forced to be ephemeral. + """ + return self._response_message_ephemeral diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 37904a580a..4c9f325987 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -272,3 +272,24 @@ class EditApplicationCommand(TypedDict): _StringApplicationIntegrationType = Literal["0", "1"] AuthorizingIntegrationOwners = Dict[_StringApplicationIntegrationType, Snowflake] + + +class InteractionCallbackResponse(TypedDict): + interaction: InteractionCallback + resource: NotRequired[InteractionCallbackResource] + + +class InteractionCallback(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 InteractionCallbackResource(TypedDict): + type: InteractionResponseType + # This is not fully typed as activities are out of scope + activity_instance: NotRequired[dict] + message: NotRequired[Message] diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 1661b1bb67..de85fce179 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -537,6 +537,10 @@ def create_interaction_response( webhook_token=token, ) + params: dict[str, Any] = { + "with_response": "true", + } + return self.request( route, session=session, @@ -544,6 +548,7 @@ def create_interaction_response( proxy_auth=proxy_auth, files=files, multipart=form, + params=params, ) def get_original_interaction_response(