Skip to content

feat: Implement with_response For Interaction Callbacks #2711

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
152 changes: 109 additions & 43 deletions discord/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"MessageInteraction",
"InteractionMetadata",
"AuthorizingIntegrationOwners",
"InteractionCallback",
)

if TYPE_CHECKING:
Expand All @@ -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
Expand Down Expand Up @@ -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, ...] = (
Expand All @@ -172,6 +179,7 @@ class Interaction:
"entitlements",
"context",
"authorizing_integration_owners",
"callback",
"_channel_data",
"_message_data",
"_guild_data",
Expand All @@ -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):
Expand Down Expand Up @@ -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|
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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,
Expand All @@ -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|
Expand All @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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
21 changes: 21 additions & 0 deletions discord/types/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
5 changes: 5 additions & 0 deletions discord/webhook/async_.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,13 +537,18 @@ def create_interaction_response(
webhook_token=token,
)

params: dict[str, Any] = {
"with_response": "true",
}

return self.request(
route,
session=session,
proxy=proxy,
proxy_auth=proxy_auth,
files=files,
multipart=form,
params=params,
)

def get_original_interaction_response(
Expand Down