Repository: python-discord/bot

Issue:
Show the result of !eval in #bot-commands when it is used in a channel where it is not allowed.
## Description
When the command `!help` is used in a channel such as `#python-general`, Python prints out the result in `#bot-commands`. However, when a command like `!eval` is used, Python simply tells the user that they can't do that here. It would be nice if Python showed the results of `!eval` in `#bot-commands` the same way it does for `!help`.

## Reasoning
It would enhance the user experience because they wouldn't have to type it all over again.

## Proposed Implementation
The same way Python currently prints the output of `!help` in `#bot-commands` when it's used in a channel where that's not allowed, it should do the same with `!eval`.

## Alternate Ideas
The only alternate implementation that I can think of is the current one, where someone is forced to do it again in `#bot-commands.`

## Would you like to implement this yourself?
- [ ] I'd like to implement this feature myself
- [x] Anyone can implement this feature



Discussion:
Sounds like a good idea to me.
I'd like to implement this.
Would it make sense to implement this throughout the entire bot for all commands?
No, we should decide on commands individually. It might be that it is okay to be done on all commands, but we should list them first and consider each.
Agreed. I used `!eval` as an example, because that's one where sometimes you spend a while typing out code. For all of them, it could be fine (I don't know all the commands, so I couldn't tell you), but it's at least worth it to consider each command separately.

Resolving patch:
diff --git a/bot/decorators.py b/bot/decorators.py
--- a/bot/decorators.py
+++ b/bot/decorators.py
@@ -107,11 +107,19 @@ async def predicate(ctx: Context) -> bool:
     return commands.check(predicate)
 
 
-def redirect_output(destination_channel: int, bypass_roles: t.Container[int] = None) -> t.Callable:
+def redirect_output(
+    destination_channel: int,
+    bypass_roles: t.Optional[t.Container[int]] = None,
+    channels: t.Optional[t.Container[int]] = None,
+    categories: t.Optional[t.Container[int]] = None,
+    ping_user: bool = True
+) -> t.Callable:
     """
     Changes the channel in the context of the command to redirect the output to a certain channel.
 
-    Redirect is bypassed if the author has a role to bypass redirection.
+    Redirect is bypassed if the author has a bypass role or if it is in a channel that can bypass redirection.
+
+    If ping_user is False, it will not send a message in the destination channel.
 
     This decorator must go before (below) the `command` decorator.
     """
@@ -119,7 +127,7 @@ def wrap(func: types.FunctionType) -> types.FunctionType:
         @command_wraps(func)
         async def inner(self: Cog, ctx: Context, *args, **kwargs) -> None:
             if ctx.channel.id == destination_channel:
-                log.trace(f"Command {ctx.command.name} was invoked in destination_channel, not redirecting")
+                log.trace(f"Command {ctx.command} was invoked in destination_channel, not redirecting")
                 await func(self, ctx, *args, **kwargs)
                 return
 
@@ -128,12 +136,24 @@ async def inner(self: Cog, ctx: Context, *args, **kwargs) -> None:
                 await func(self, ctx, *args, **kwargs)
                 return
 
+            elif channels and ctx.channel.id not in channels:
+                log.trace(f"{ctx.author} used {ctx.command} in a channel that can bypass output redirection")
+                await func(self, ctx, *args, **kwargs)
+                return
+
+            elif categories and ctx.channel.category.id not in categories:
+                log.trace(f"{ctx.author} used {ctx.command} in a category that can bypass output redirection")
+                await func(self, ctx, *args, **kwargs)
+                return
+
             redirect_channel = ctx.guild.get_channel(destination_channel)
             old_channel = ctx.channel
 
             log.trace(f"Redirecting output of {ctx.author}'s command '{ctx.command.name}' to {redirect_channel.name}")
             ctx.channel = redirect_channel
-            await ctx.channel.send(f"Here's the output of your command, {ctx.author.mention}")
+
+            if ping_user:
+                await ctx.send(f"Here's the output of your command, {ctx.author.mention}")
             asyncio.create_task(func(self, ctx, *args, **kwargs))
 
             message = await old_channel.send(
diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py
--- a/bot/exts/utils/snekbox.py
+++ b/bot/exts/utils/snekbox.py
@@ -13,7 +13,7 @@
 
 from bot.bot import Bot
 from bot.constants import Categories, Channels, Roles, URLs
-from bot.decorators import not_in_blacklist
+from bot.decorators import redirect_output
 from bot.utils import send_to_paste_service
 from bot.utils.messages import wait_for_deletion
 
@@ -280,7 +280,13 @@ async def get_code(self, message: Message) -> Optional[str]:
 
     @command(name="eval", aliases=("e",))
     @guild_only()
-    @not_in_blacklist(channels=NO_EVAL_CHANNELS, categories=NO_EVAL_CATEGORIES, override_roles=EVAL_ROLES)
+    @redirect_output(
+        destination_channel=Channels.bot_commands,
+        bypass_roles=EVAL_ROLES,
+        categories=NO_EVAL_CATEGORIES,
+        channels=NO_EVAL_CHANNELS,
+        ping_user=False
+    )
     async def eval_command(self, ctx: Context, *, code: str = None) -> None:
         """
         Run Python code and get the results.


Test patch: