Repository: disnakedev/disnake
Issue: interactions: possible race condition when responding

### Summary

possible race condition when responding

### Reproduction Steps

had two responses made at the same time

### Minimal Reproducible Code

```python
@bot.slash_command(name="race-con")
async def race_condition(inter: disnake.CommandInteraction):
    async def maybe_respond_after_two_seconds(num):
        await asyncio.sleep(2)
        if inter.response.is_done():
            return
        await inter.response.send_message(f"hi {num}")

    for num in range(2):
        bot.loop.create_task(maybe_respond_after_two_seconds(num))

    @commands.user_command(name="Give sudo")
    async def sud(self, inter: disnake.UserCommandInteraction):
        await inter.send("hello hi how are you hello hi")
```


### Expected Results

should receive an InteractionAlreadyResponded error, never an HTTPException, 

### Actual Results

sometimes receive an HTTPException, but not always

### Intents

all

### System Information

- Python v3.8.10-final
- disnake v2.5.0-alpha
    - disnake pkg_resources: v2.5.0a4138+g1f0a5068
- aiohttp v3.8.1
- system info: Linux 5.10.102.1-microsoft-standard-WSL2 # 1 SMP Wed Mar 2 00:30:59 UTC 2022

### Checklist

- [X] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

This is a race condition due to internal _responded flag being checked, but then the event loop receives both requests before the _responded flag is set. To fix this, we can use a lock around interaction responses and only allow one item to be in the lock at a time.

Changed files:
disnake/interactions/base.py

diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py
index 9419213d..593376fa 100644
--- a/disnake/interactions/base.py
+++ b/disnake/interactions/base.py
@@ -76,6 +76,39 @@ if TYPE_CHECKING:
 MISSING: Any = utils.MISSING
 
 
+class _ResponseLock:
+    __slots__ = (
+        "_response",
+        "_lock",
+    )
+
+    def __init__(self, response: InteractionResponse) -> None:
+        self._response = response
+        self._lock = asyncio.Lock()
+
+    async def __aenter__(self):
+        # when we acquire this manager, we are attempting to make a request
+        # the problem is we don't know if the request will success or not
+        # to get around this, we wait and see if it was successful.
+        # if it was successful, we raise an InteractionResponded exception
+        await self._lock.acquire()
+        if self._response._response_type is not None:
+            self._lock.release()
+            raise InteractionResponded(self._response._parent)
+
+    async def __aexit__(self, exc_type, exc, tb):
+        # we are no longer making a request so we release the lock
+        # however, we only want to set responded to True if it was success
+        # eg did not raise an Exception
+        self._lock.release()
+        if exc_type is not None:
+            return
+        # todo: set that we responded somehow
+
+    def is_responding(self):
+        return self._lock.locked()
+
+
 class Interaction:
     """A base class representing a user-initiated Discord interaction.
 
@@ -628,6 +661,14 @@ class Interaction:
         ValueError
             The length of ``embeds`` was invalid.
         """
+        # this is done before determining which sender to use
+        # if we are currently responding we should use the followup
+        # but there is a chance the response will fail and therefore
+        # we should send the response
+        if self.response._lock.is_responding():
+            async with self.response._lock:
+                pass
+
         if self.response._response_type is not None:
             sender = self.followup.send
         else:
@@ -658,12 +699,14 @@ class InteractionResponse:
 
     __slots__: Tuple[str, ...] = (
         "_parent",
+        "_lock",
         "_response_type",
     )
 
     def __init__(self, parent: Interaction):
         self._parent: Interaction = parent
         self._response_type: Optional[InteractionResponseType] = None
+        self._lock = _ResponseLock(self)
 
     @property
     def type(self) -> Optional[InteractionResponseType]:
@@ -672,6 +715,7 @@ class InteractionResponse:
         .. versionadded:: 2.6
         """
         return self._response_type
+        self._lock = _ResponseLock(self)
 
     def is_done(self) -> bool:
         """Whether an interaction response has been done before.
@@ -765,14 +809,15 @@ class InteractionResponse:
                 data["flags"] |= MessageFlags.ephemeral.flag
 
         adapter = async_context.get()
-        await adapter.create_interaction_response(
-            parent.id,
-            parent.token,
-            session=parent._session,
-            type=defer_type.value,
-            data=data or None,
-        )
-        self._response_type = defer_type
+        async with self._lock:
+            await adapter.create_interaction_response(
+                parent.id,
+                parent.token,
+                session=parent._session,
+                type=defer_type.value,
+                data=data or None,
+            )
+            self._response_type = defer_type
 
     async def pong(self) -> None:
         """|coro|
@@ -795,13 +840,14 @@ class InteractionResponse:
         if parent.type is InteractionType.ping:
             adapter = async_context.get()
             response_type = InteractionResponseType.pong
-            await adapter.create_interaction_response(
-                parent.id,
-                parent.token,
-                session=parent._session,
-                type=response_type.value,
-            )
-            self._response_type = response_type
+            async with self._lock:
+                await adapter.create_interaction_response(
+                    parent.id,
+                    parent.token,
+                    session=parent._session,
+                    type=response_type.value,
+                )
+                self._response_type = response_type
 
     async def send_message(
         self,
@@ -942,14 +988,16 @@ class InteractionResponse:
         adapter = async_context.get()
         response_type = InteractionResponseType.channel_message
         try:
-            await adapter.create_interaction_response(
-                parent.id,
-                parent.token,
-                session=parent._session,
-                type=response_type.value,
-                data=payload,
-                files=files or None,
-            )
+            async with self._lock:
+                await adapter.create_interaction_response(
+                    parent.id,
+                    parent.token,
+                    session=parent._session,
+                    type=response_type.value,
+                    data=payload,
+                    files=files or None,
+                )
+                self._response_type = response_type
         except NotFound as e:
             if e.code == 10062:
                 raise InteractionTimedOut(self._parent) from e
@@ -959,8 +1007,6 @@ class InteractionResponse:
                 for f in files:
                     f.close()
 
-        self._response_type = response_type
-
         if view is not MISSING:
             if ephemeral and view.timeout is None:
                 view.timeout = 15 * 60.0
@@ -1125,14 +1171,16 @@ class InteractionResponse:
         adapter = async_context.get()
         response_type = InteractionResponseType.message_update
         try:
-            await adapter.create_interaction_response(
-                parent.id,
-                parent.token,
-                session=parent._session,
-                type=response_type.value,
-                data=payload,
-                files=files,
-            )
+            async with self._lock:
+                await adapter.create_interaction_response(
+                    parent.id,
+                    parent.token,
+                    session=parent._session,
+                    type=response_type.value,
+                    data=payload,
+                    files=files,
+                )
+                self._response_type = response_type
         finally:
             if files:
                 for f in files:
@@ -1141,8 +1189,6 @@ class InteractionResponse:
         if view and not view.is_finished():
             state.store_view(view, message.id)
 
-        self._response_type = response_type
-
     async def autocomplete(self, *, choices: Choices) -> None:
         """|coro|
 
@@ -1185,15 +1231,15 @@ class InteractionResponse:
         parent = self._parent
         adapter = async_context.get()
         response_type = InteractionResponseType.application_command_autocomplete_result
-        await adapter.create_interaction_response(
-            parent.id,
-            parent.token,
-            session=parent._session,
-            type=response_type.value,
-            data={"choices": choices_data},
-        )
-
-        self._response_type = response_type
+        async with self._lock:
+            await adapter.create_interaction_response(
+                parent.id,
+                parent.token,
+                session=parent._session,
+                type=response_type.value,
+                data={"choices": choices_data},
+            )
+            self._response_type = response_type
 
     @overload
     async def send_modal(self, modal: Modal) -> None:
@@ -1285,14 +1331,15 @@ class InteractionResponse:
 
         adapter = async_context.get()
         response_type = InteractionResponseType.modal
-        await adapter.create_interaction_response(
-            parent.id,
-            parent.token,
-            session=parent._session,
-            type=response_type.value,
-            data=modal_data,  # type: ignore
-        )
-        self._response_type = response_type
+        async with self._lock:
+            await adapter.create_interaction_response(
+                parent.id,
+                parent.token,
+                session=parent._session,
+                type=response_type.value,
+                data=modal_data,  # type: ignore
+            )
+            self._response_type = response_type
 
         if modal is not None:
             parent._state.store_modal(parent.author.id, modal)