fix(discord): handle expired slash defer interactions

This commit is contained in:
Alix-007 2026-05-28 11:57:42 +08:00 committed by Teknium
parent acfefa4fda
commit b9d9b8aad6
2 changed files with 67 additions and 2 deletions

View file

@ -1626,6 +1626,31 @@ class DiscordAdapter(BasePlatformAdapter):
return True
return False
@staticmethod
def _is_discord_unknown_interaction(exc: BaseException) -> bool:
"""True for Discord's expired interaction token error."""
code = getattr(exc, "code", None)
if code is None:
data = getattr(exc, "data", None)
if isinstance(data, dict):
code = data.get("code")
try:
code = int(code)
except (TypeError, ValueError):
code = None
status = getattr(exc, "status", None)
response = getattr(exc, "response", None)
if status is None and response is not None:
status = getattr(response, "status", None) or getattr(response, "status_code", None)
try:
status = int(status)
except (TypeError, ValueError):
status = None
message = str(exc).lower()
return code == 10062 or (status == 404 and "unknown interaction" in message)
def _command_sync_mutation_interval_seconds(self) -> float:
return _DISCORD_COMMAND_SYNC_MUTATION_INTERVAL_SECONDS
@ -3964,9 +3989,22 @@ class DiscordAdapter(BasePlatformAdapter):
if not await self._check_slash_authorization(interaction, command_text):
return
await interaction.response.defer(ephemeral=True)
deferred_response = False
try:
await interaction.response.defer(ephemeral=True)
deferred_response = True
except Exception as e:
if not self._is_discord_unknown_interaction(e):
raise
logger.warning(
"[Discord] slash %s: interaction expired before defer. "
"Executing command anyway, skipping interaction followup.",
command_text,
)
event = self._build_slash_event(interaction, command_text)
await self.handle_message(event)
if not deferred_response:
return
try:
if followup_msg:
await interaction.edit_original_response(content=followup_msg)

View file

@ -158,6 +158,34 @@ async def test_registers_native_restart_slash_command(adapter):
)
@pytest.mark.asyncio
async def test_run_simple_slash_executes_when_defer_interaction_expired(adapter):
class UnknownInteraction(Exception):
status = 404
code = 10062
interaction = SimpleNamespace(
channel=_FakeTextChannel(channel_id=123, name="general"),
channel_id=123,
guild_id=456,
user=SimpleNamespace(id=42, name="Jezza", display_name="Jezza"),
response=SimpleNamespace(defer=AsyncMock(side_effect=UnknownInteraction("Unknown interaction"))),
edit_original_response=AsyncMock(),
delete_original_response=AsyncMock(),
)
adapter.handle_message = AsyncMock()
await adapter._run_simple_slash(interaction, "/reset", "Session reset~")
interaction.response.defer.assert_awaited_once_with(ephemeral=True)
adapter.handle_message.assert_awaited_once()
event = adapter.handle_message.await_args.args[0]
assert event.text == "/reset"
assert event.source.chat_id == "123"
interaction.edit_original_response.assert_not_awaited()
interaction.delete_original_response.assert_not_awaited()
# ------------------------------------------------------------------
# Auto-registration from COMMAND_REGISTRY
# ------------------------------------------------------------------
@ -1117,4 +1145,3 @@ def test_register_skill_command_autocomplete_filters_by_name_and_description(ada
# (covered in other tests). The autocomplete filter itself is exercised
# via direct function call in the real-discord integration path.
assert skill_cmd.callback is not None