Repository: Pycord-Development/pycord

Issue:
bot.before_invoke function runs twice with slash sub commands causing a whole bunch of issues
### Summary

the bot.before_invoke function gets called twice during the invocation of a slash sub command

### Reproduction Steps

This seems to only occur on master and not stable 2.4.1
1. Have a pre-invocation function with the `@bot.before_invoke` decorator
2. Have any slash command group
3. add a print statement to the pre-invocation function or add context.defer()
4. print statement will print twice or `InteractionResponded` will be raised

### Minimal Reproducible Code

```python
# in main file
#...

client: bridge.Bot = bridge.Bot(command_prefix=config.prefix, intents=intents, description=bot_desc, help_command=information.RevnobotHelp3(), debug_guilds=config.slash_guilds,max_messages=10**3, enable_debug_events=config.debug_mode)

# ...

@client.before_invoke
async def pre_invocation(ctx):
    print("i executed")

# in cog 

test_group = discord.SlashCommandGroup('test-group', "Hello Worlds")

@test_group.command(name="test1", description="Hello World 1")
@commands.cooldown(**config.default_cooldown_options)
@commands.bot_has_permissions(send_messages=True, attach_files=True)
async def test_1(self, ctx: discord.ApplicationContext):
    await ctx.respond("Hello World 1")
```


### Expected Results

`i executed` should only print once meaning

### Actual Results

`i executed` prints twice
or the following traceback if you use ctx.defer()
```python
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 131, in wrapped
    ret = await coro(arg)
          ^^^^^^^^^^^^^^^

  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 1363, in _invoke
    await command.invoke(ctx)

  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 378, in invoke
    await self.prepare(ctx)

  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 310, in prepare
    await self.call_before_hooks(ctx)

  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 535, in call_before_hooks
    await hook(ctx)

  File "Revnobot/main.py", line 116, in pre_invocation
    await utils.pre_invocation(ctx)

  File "Revnobot/utils.py", line 147, in pre_invocation
    await ctx.defer()

  File "Revnobot/venv/lib/python3.11/site-packages/discord/ext/bridge/context.py", line 113, in defer
    return await self._defer(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "Revnobot/venv/lib/python3.11/site-packages/discord/ext/bridge/context.py", line 149, in _defer
    return await self._get_super("defer")(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "Revnobot/venv/lib/python3.11/site-packages/discord/interactions.py", line 705, in defer
    raise InteractionResponded(self._parent)
```

### Intents

all

### System Information

- Python v3.11.3-final
- py-cord v2.4.1-final
- aiohttp v3.8.4
- system info: Linux 6.3.6-arch1-1 #1 SMP PREEMPT_DYNAMIC Mon, 05 Jun 2023 15:12:57 +0000


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

_No response_


Discussion:
Did you also set a `cog_before_invoke` method inside the cog? Can't repro normally.
I will have to check
Not sure if this helps but I managed to capture stack traces both times the function was called
The difference appears to be near the end.
Execution 1:
```python
  File "Revnobot/main.py", line 154, in <module>
    client.run(config.token)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/client.py", line 753, in run
    loop.run_forever()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 607, in run_forever
    self._run_once()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 1922, in _run_once
    handle._run()
  File "/usr/lib/python3.11/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/client.py", line 399, in _run_event
    await coro(*args, **kwargs)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/bot.py", line 1185, in on_interaction
    await self.process_application_commands(interaction)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/bot.py", line 867, in process_application_commands
    await self.invoke_application_command(ctx)
  File "/Revnobot/venv/lib/python3.11/site-packages/discord/ext/bridge/bot.py", line 176, in invoke_application_command
    await ctx.command.invoke(ctx)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 378, in invoke
    await self.prepare(ctx)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 310, in prepare
    await self.call_before_hooks(ctx)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 535, in call_before_hooks
    await hook(ctx)
  File "Revnobot/main.py", line 116, in pre_invocation
    ctx, "Execution Stack", f'\```python\n{("".join(traceback.format_stack())).replace("\```", "")}\```'))
```
Execution 2:
```python
  File "Revnobot/main.py", line 154, in <module>
    client.run(config.token)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/client.py", line 753, in run
    loop.run_forever()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 607, in run_forever
    self._run_once()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 1922, in _run_once
    handle._run()
  File "/usr/lib/python3.11/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/client.py", line 399, in _run_event
    await coro(*args, **kwargs)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/bot.py", line 1185, in on_interaction
    await self.process_application_commands(interaction)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/bot.py", line 867, in process_application_commands
    await self.invoke_application_command(ctx)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/ext/bridge/bot.py", line 176, in invoke_application_command
    await ctx.command.invoke(ctx)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 381, in invoke
    await injected(ctx)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 131, in wrapped
    ret = await coro(arg)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 1363, in _invoke
    await command.invoke(ctx)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 378, in invoke
    await self.prepare(ctx)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 310, in prepare
    await self.call_before_hooks(ctx)
  File "Revnobot/venv/lib/python3.11/site-packages/discord/commands/core.py", line 535, in call_before_hooks
    await hook(ctx)
  File "Revnobot/main.py", line 116, in pre_invocation
    ctx, "Execution Stack", f'\```python\n{("".join(traceback.format_stack())).replace("\```", "")}\```'))
```


I think #2091 might play a part in this. ~~why do my PRs always have gotchas~~

From my understanding, Slash command group is also considered a command. So `before_invoke` will also be called for it.


Can you check if this issue occurred before that pr?
Can confirm that it doesn't occur on #2117 (the commit before #2091) and that it occurs on #2091 and later
Considering how the library deals with Slash groups, this seems logical. A Slash group is just another "command". And #2091 adds features that make it even more like a command. I also intend to add a `ext.commands` style callback to Slash groups. 

So it only makes sense to run functions set using `@bot.before_invoke` before the Slash group too. 

I will wait for core devs to decide whether this is a feature or a bug. 

PS - using `InteractionResponse` related methods like `ctx.defer()` inside `before_invoke` is not a good idea anyways. You might want to use checks or smth
> Considering how the library deals with Slash groups, this seems logical. A Slash group is just another "command". And #2091 adds features that make it even more like a command. I also intend to add a `ext.commands` style callback to Slash groups. 
> 
> So it only makes sense to run functions set using `@bot.before_invoke` before the Slash group too. 
> 
> I will wait for core devs to decide whether this is a feature or a bug. 
> 
> PS - using `InteractionResponse` related methods like `ctx.defer()` inside `before_invoke` is not a good idea anyways. You might want to use checks or smth

Since the slash group itself is not being invoked, before_invoke should not be called for it

Resolving patch:
diff --git a/discord/commands/core.py b/discord/commands/core.py
--- a/discord/commands/core.py
+++ b/discord/commands/core.py
@@ -1368,6 +1368,28 @@ async def invoke_autocomplete_callback(self, ctx: AutocompleteContext) -> None:
         ctx.interaction.data = option
         await command.invoke_autocomplete_callback(ctx)
 
+    async def call_before_hooks(self, ctx: ApplicationContext) -> None:
+        # only call local hooks
+        cog = self.cog
+        if self._before_invoke is not None:
+            # should be cog if @commands.before_invoke is used
+            instance = getattr(self._before_invoke, "__self__", cog)
+            # __self__ only exists for methods, not functions
+            # however, if @command.before_invoke is used, it will be a function
+            if instance:
+                await self._before_invoke(instance, ctx)  # type: ignore
+            else:
+                await self._before_invoke(ctx)  # type: ignore
+
+    async def call_after_hooks(self, ctx: ApplicationContext) -> None:
+        cog = self.cog
+        if self._after_invoke is not None:
+            instance = getattr(self._after_invoke, "__self__", cog)
+            if instance:
+                await self._after_invoke(instance, ctx)  # type: ignore
+            else:
+                await self._after_invoke(ctx)  # type: ignore
+
     def walk_commands(self) -> Generator[SlashCommand | SlashCommandGroup, None, None]:
         """An iterator that recursively walks through all slash commands and groups in this group.
 


Test patch: