From 3dcd834af317309a7f402196d4a9306c08288c52 Mon Sep 17 00:00:00 2001 From: Mihito Date: Thu, 17 Feb 2022 18:48:48 +0100 Subject: [PATCH 01/10] Fix race condition in interactions.py --- discord/interactions.py | 112 +++++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 37 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index abca1a6ba5..0dc21aab03 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -27,7 +27,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +import asyncio +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, Coroutine from . import utils from .channel import ChannelType, PartialMessageable @@ -429,11 +430,13 @@ class InteractionResponse: __slots__: Tuple[str, ...] = ( "_responded", "_parent", + "_response_lock", ) def __init__(self, parent: Interaction): self._parent: Interaction = parent self._responded: bool = False + self._response_lock = asyncio.Lock() def is_done(self) -> bool: """:class:`bool`: Indicates whether an interaction response has been done before. @@ -482,12 +485,14 @@ async def defer(self, *, ephemeral: bool = False) -> None: if defer_type: adapter = async_context.get() - await adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=defer_type, - data=data, + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=defer_type, + data=data, + ) ) self._responded = True @@ -511,11 +516,13 @@ async def pong(self) -> None: parent = self._parent if parent.type is InteractionType.ping: adapter = async_context.get() - await adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.pong.value, + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.pong.value, + ) ) self._responded = True @@ -631,13 +638,15 @@ async def send_message( parent = self._parent adapter = async_context.get() try: - await adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.channel_message.value, - data=payload, - files=files, + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.channel_message.value, + data=payload, + files=files, + ) ) finally: if files: @@ -727,12 +736,14 @@ async def edit_message( state.prevent_view_updates_for(message_id) payload["components"] = [] if view is None else view.to_components() adapter = async_context.get() - await adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.message_update.value, - data=payload, + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.message_update.value, + data=payload, + ) ) if view and not view.is_finished(): @@ -773,12 +784,14 @@ async def send_autocomplete_result( payload = {"choices": [c.to_dict() for c in choices]} adapter = async_context.get() - await adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - type=InteractionResponseType.auto_complete_result.value, - data=payload, + await self._locked_response( + adapter.create_interaction_response( + parent.id, + parent.token, + session=parent._session, + type=InteractionResponseType.auto_complete_result.value, + data=payload, + ) ) self._responded = True @@ -789,16 +802,41 @@ async def send_modal(self, modal: Modal): payload = modal.to_dict() adapter = async_context.get() - await adapter.create_interaction_response( - self._parent.id, - self._parent.token, - session=self._parent._session, - type=InteractionResponseType.modal.value, - data=payload, + await self._locked_response( + adapter.create_interaction_response( + self._parent.id, + self._parent.token, + session=self._parent._session, + type=InteractionResponseType.modal.value, + data=payload, + ) ) self._responded = True self._parent._state.store_modal(modal, self._parent.user.id) + async def _locked_response(self, coro: Coroutine[Any]): + """|coro| + + Wraps a response and makes sure that is locked while executing + + Parameters + ----------- + coro: + The coroutine to wrap + + Raises + ------- + HTTPException + Deferring the interaction failed. + InteractionResponded + This interaction has already been responded to before. + """ + async with self._response_lock: + if self.is_done(): + coro.close() # Cleanup unawaited coroutine + raise InteractionResponded(self._parent) + await coro + class _InteractionMessageState: __slots__ = ("_parent", "_interaction") From af6bfd5e4cb9706b8caaafe7c90c4582c16eaf29 Mon Sep 17 00:00:00 2001 From: Mihito Date: Thu, 17 Feb 2022 18:52:24 +0100 Subject: [PATCH 02/10] Fixed ApplicationContext.respond --- discord/commands/context.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/discord/commands/context.py b/discord/commands/context.py index 716cb49f2f..0824d0678b 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -219,10 +219,21 @@ def unselected_options(self) -> Optional[List[Option]]: def respond(self) -> Callable[..., Awaitable[Union[Interaction, WebhookMessage]]]: """Callable[..., Union[:class:`~.Interaction`, :class:`~.Webhook`]]: Sends either a response or a followup response depending if the interaction has been responded to yet or not.""" - if not self.interaction.response.is_done(): - return self.interaction.response.send_message # self.response - else: - return self.followup.send # self.send_followup + + # This technically can still be effected by the race condition. Solving that would include a breaking change + # But now it will raise InteractionResponded not the unexpected HTTP exception + # So we return a wrapper that retires when that exception got raised + + async def wrapper(*args, **kwargs): + try: + if not self.interaction.response.is_done(): + return await self.interaction.response.send_message(*args, **kwargs) # self.response + else: + return await self.followup.send(*args, **kwargs) # self.send_followup + except discord.errors.InteractionResponded: + await self.followup.send(*args, **kwargs) + + return wrapper @property def send_response(self) -> Callable[..., Awaitable[Interaction]]: From b35b0a7fd7a4aaa5a4f6033b4d81634fdbe2be95 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sun, 3 Apr 2022 21:44:18 +0200 Subject: [PATCH 03/10] Update discord/commands/context.py Co-authored-by: Jay Turner --- discord/commands/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/commands/context.py b/discord/commands/context.py index bd8367e472..6906d07dc7 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -236,7 +236,7 @@ async def wrapper(*args, **kwargs): else: return await self.followup.send(*args, **kwargs) # self.send_followup except discord.errors.InteractionResponded: - await self.followup.send(*args, **kwargs) + return await self.followup.send(*args, **kwargs) return wrapper From 543b1062f702aa846fc359bf78bd8b6d2a5da789 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sat, 9 Apr 2022 20:33:07 +0200 Subject: [PATCH 04/10] Update discord/commands/context.py Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- discord/commands/context.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/discord/commands/context.py b/discord/commands/context.py index c9c70e89ea..ec1930e023 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -220,9 +220,8 @@ def send_modal(self) -> Callable[..., Awaitable[Interaction]]: """Sends a modal dialog to the user who invoked the interaction.""" return self.interaction.response.send_modal - @property - def respond(self) -> Callable[..., Awaitable[Union[Interaction, WebhookMessage]]]: - """Callable[..., Union[:class:`~.Interaction`, :class:`~.Webhook`]]: Sends either a response + async def respond(self) -> Union[Interaction, WebhookMessage]: + """Sends either a response or a followup response depending if the interaction has been responded to yet or not.""" or a followup response depending if the interaction has been responded to yet or not.""" # This technically can still be effected by the race condition. Solving that would include a breaking change From f334491aa3d57105b98287d2fc5de45081edabf6 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sat, 9 Apr 2022 20:33:24 +0200 Subject: [PATCH 05/10] Update discord/commands/context.py Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- discord/commands/context.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/discord/commands/context.py b/discord/commands/context.py index ec1930e023..0d9c0cf8ce 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -222,22 +222,13 @@ def send_modal(self) -> Callable[..., Awaitable[Interaction]]: async def respond(self) -> Union[Interaction, WebhookMessage]: """Sends either a response or a followup response depending if the interaction has been responded to yet or not.""" - or a followup response depending if the interaction has been responded to yet or not.""" - - # This technically can still be effected by the race condition. Solving that would include a breaking change - # But now it will raise InteractionResponded not the unexpected HTTP exception - # So we return a wrapper that retires when that exception got raised - - async def wrapper(*args, **kwargs): - try: - if not self.interaction.response.is_done(): - return await self.interaction.response.send_message(*args, **kwargs) # self.response - else: - return await self.followup.send(*args, **kwargs) # self.send_followup - except discord.errors.InteractionResponded: - return await self.followup.send(*args, **kwargs) - - return wrapper + try: + if not self.interaction.response.is_done(): + return await self.interaction.response.send_message(*args, **kwargs) # self.response + else: + return await self.followup.send(*args, **kwargs) # self.send_followup + except discord.errors.InteractionResponded: + return await self.followup.send(*args, **kwargs) @property def send_response(self) -> Callable[..., Awaitable[Interaction]]: From 5f05e23fa18f74c6e97dc9b59e458756d6b9164e Mon Sep 17 00:00:00 2001 From: krittick Date: Sat, 16 Apr 2022 12:50:00 -0700 Subject: [PATCH 06/10] Update discord/commands/context.py --- discord/commands/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/commands/context.py b/discord/commands/context.py index 0d9c0cf8ce..dbc40d1043 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -220,7 +220,7 @@ def send_modal(self) -> Callable[..., Awaitable[Interaction]]: """Sends a modal dialog to the user who invoked the interaction.""" return self.interaction.response.send_modal - async def respond(self) -> Union[Interaction, WebhookMessage]: + async def respond(self, *args, **kwargs) -> Union[Interaction, WebhookMessage]: """Sends either a response or a followup response depending if the interaction has been responded to yet or not.""" try: if not self.interaction.response.is_done(): From e49066a6d870099ffbf73bfe3969c7bc398eaf65 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Mon, 18 Apr 2022 00:01:26 +0200 Subject: [PATCH 07/10] Update discord/interactions.py Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- discord/interactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/interactions.py b/discord/interactions.py index 0c2d661580..22ee3c7ed4 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -841,7 +841,7 @@ async def send_modal(self, modal: Modal) -> Interaction: async def _locked_response(self, coro: Coroutine[Any]): """|coro| - Wraps a response and makes sure that is locked while executing + Wraps a response and makes sure that it's locked while executing. Parameters ----------- From d083530ab45bb032253d56680c85ec7e90a378f1 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Mon, 18 Apr 2022 00:01:42 +0200 Subject: [PATCH 08/10] Update discord/interactions.py Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- discord/interactions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 22ee3c7ed4..00a0c81dca 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -845,8 +845,8 @@ async def _locked_response(self, coro: Coroutine[Any]): Parameters ----------- - coro: - The coroutine to wrap + coro: Coroutine[Any] + The coroutine to wrap. Raises ------- From cc7a3dcd1d9fae450dc5e3e221902216dfe36fcf Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Mon, 18 Apr 2022 00:01:53 +0200 Subject: [PATCH 09/10] Update discord/interactions.py Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- discord/interactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/interactions.py b/discord/interactions.py index 00a0c81dca..1e87cf5b2b 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -857,7 +857,7 @@ async def _locked_response(self, coro: Coroutine[Any]): """ async with self._response_lock: if self.is_done(): - coro.close() # Cleanup unawaited coroutine + coro.close() # cleanup unawaited coroutine raise InteractionResponded(self._parent) await coro From de53193e16f72f5104a161b35746dc4e3aa4fd74 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Mon, 18 Apr 2022 00:02:10 +0200 Subject: [PATCH 10/10] Update discord/interactions.py Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com> --- discord/interactions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 1e87cf5b2b..c964e5ca4f 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -850,8 +850,6 @@ async def _locked_response(self, coro: Coroutine[Any]): Raises ------- - HTTPException - Deferring the interaction failed. InteractionResponded This interaction has already been responded to before. """