Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
51e40af
feat: Implement with_response For Interaction Callbacks
Icebluewolf Feb 11, 2025
0b4aa8c
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 11, 2025
04a0f27
chore: Changelog
Icebluewolf Feb 11, 2025
b14a15d
Merge branch 'with-response' of https://github.com/Icebluewolf/pycord…
Icebluewolf Feb 11, 2025
9fae95a
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 11, 2025
dbcb58c
fix: Set Correct URL Parameter Value
Icebluewolf Feb 15, 2025
426d991
feat: Add Callback To All Responses
Icebluewolf Feb 15, 2025
296436b
Merge branch 'with-response' of https://github.com/Icebluewolf/pycord…
Icebluewolf Feb 15, 2025
405cb74
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 15, 2025
202a8db
Merge branch 'master' into with-response
Icebluewolf Feb 17, 2025
25823fd
refractor: Use A callback Parameter Of Interaction
Icebluewolf May 29, 2025
8ee94ae
Merge branch 'master' into with-response
Icebluewolf May 30, 2025
d342498
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 30, 2025
6e11847
Merge branch 'master' into with-response
Icebluewolf Aug 31, 2025
aeb0463
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 31, 2025
558c18c
Merge branch 'master' into with-response
Lulalaby Aug 31, 2025
8724929
Merge branch 'master' into with-response
Lulalaby Sep 1, 2025
a1c0184
fix: changelog entry position
Lulalaby Sep 1, 2025
1cdbe30
Merge branch 'master' into with-response
Lulalaby Sep 1, 2025
27fc7db
chore: Apply suggestions from code review
Icebluewolf Sep 1, 2025
172898f
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 1, 2025
c505fbb
chore: Clarify exception message
Icebluewolf Sep 1, 2025
4b8114c
Merge branch 'with-response' of https://github.com/Icebluewolf/pycord…
Icebluewolf Sep 1, 2025
437e811
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 1, 2025
dca462a
Merge branch 'master' into with-response
Lulalaby Sep 1, 2025
c3f9958
Merge branch 'master' into with-response
Lulalaby Sep 1, 2025
5aace18
feat: Add __repr__ to InteractionCallback
Icebluewolf Sep 3, 2025
dbe357f
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 3, 2025
cbf4717
Merge branch 'master' into with-response
Paillat-dev Sep 3, 2025
492c54e
Merge branch 'master' into with-response
Paillat-dev Sep 3, 2025
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ These changes are available on the `master` branch, but have not yet been releas

### Added

- 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))

### Changed

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

if TYPE_CHECKING:
Expand All @@ -83,7 +84,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 @@ -153,6 +155,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
command: Optional[:class:`ApplicationCommand`]
The command that this interaction belongs to.

Expand Down Expand Up @@ -189,6 +196,7 @@ class Interaction:
"entitlements",
"context",
"authorizing_integration_owners",
"callback",
"command",
"view",
"modal",
Expand All @@ -213,6 +221,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 @@ -860,18 +869,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 @@ -894,17 +906,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 @@ -1048,16 +1079,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 @@ -1074,6 +1107,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 @@ -1213,16 +1247,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 @@ -1235,6 +1271,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 @@ -1270,7 +1307,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 @@ -1283,6 +1320,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 @@ -1309,7 +1347,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 @@ -1321,6 +1359,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 @@ -1348,7 +1387,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 @@ -1359,9 +1398,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 @@ -1380,7 +1420,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 @@ -1704,3 +1744,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 @@ -273,3 +273,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 @@ -545,13 +545,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
Loading