aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/exts/backend/error_handler.py59
-rw-r--r--tests/bot/exts/backend/test_error_handler.py7
2 files changed, 51 insertions, 15 deletions
diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py
index faa39db5d..5cf07613d 100644
--- a/bot/exts/backend/error_handler.py
+++ b/bot/exts/backend/error_handler.py
@@ -1,10 +1,12 @@
import copy
import difflib
-from discord import Embed, Forbidden, Member
+import discord
+from discord import ButtonStyle, Embed, Forbidden, Interaction, Member, User
from discord.ext.commands import ChannelNotFound, Cog, Context, TextChannelConverter, VoiceChannelConverter, errors
from pydis_core.site_api import ResponseCodeError
from pydis_core.utils.error_handling import handle_forbidden_from_block
+from pydis_core.utils.interactions import DeleteMessageButton, ViewWithUserAndRoleCheck
from sentry_sdk import push_scope
from bot.bot import Bot
@@ -16,6 +18,35 @@ from bot.utils.checks import ContextCheckFailure
log = get_logger(__name__)
+class HelpEmbedView(ViewWithUserAndRoleCheck):
+ """View to allow showing the help command for command error responses."""
+
+ def __init__(self, help_embed: Embed, owner: User | Member):
+ super().__init__(allowed_roles=MODERATION_ROLES, allowed_users=[owner.id])
+ self.help_embed = help_embed
+
+ self.delete_button = DeleteMessageButton()
+ self.add_item(self.delete_button)
+
+ async def interaction_check(self, interaction: Interaction) -> bool:
+ """Overriden check to allow anyone to use the help button."""
+ if (interaction.data or {}).get("custom_id") == self.help_button.custom_id:
+ log.trace(
+ "Allowed interaction by %s (%d) on %d as interaction was with the help button.",
+ interaction.user,
+ interaction.user.id,
+ interaction.message.id,
+ )
+ return True
+
+ return await super().interaction_check(interaction)
+
+ @discord.ui.button(label="Help", style=ButtonStyle.primary)
+ async def help_button(self, interaction: Interaction, button: discord.ui.Button) -> None:
+ """Send an ephemeral message with the contents of the help command."""
+ await interaction.response.send_message(embed=self.help_embed, ephemeral=True)
+
+
class ErrorHandler(Cog):
"""Handles errors emitted from commands."""
@@ -117,15 +148,6 @@ class ErrorHandler(Cog):
# ExtensionError
await self.handle_unexpected_error(ctx, e)
- async def send_command_help(self, ctx: Context) -> None:
- """Return a prepared `help` command invocation coroutine."""
- if ctx.command:
- self.bot.help_command.context = ctx
- await ctx.send_help(ctx.command)
- return
-
- await ctx.send_help()
-
async def try_silence(self, ctx: Context) -> bool:
"""
Attempt to invoke the silence or unsilence command if invoke with matches a pattern.
@@ -300,8 +322,21 @@ class ErrorHandler(Cog):
)
self.bot.stats.incr("errors.other_user_input_error")
- await ctx.send(embed=embed)
- await self.send_command_help(ctx)
+ await self.send_error_with_help(ctx, embed)
+
+ async def send_error_with_help(self, ctx: Context, error_embed: Embed) -> None:
+ """Send error message, with button to show command help."""
+ # Fall back to just sending the error embed if the custom help cog isn't loaded yet.
+ # ctx.command shouldn't be None here, but check just to be safe.
+ help_embed_creator = getattr(self.bot.help_command, "command_formatting", None)
+ if not help_embed_creator or not ctx.command:
+ await ctx.send(embed=error_embed)
+ return
+
+ self.bot.help_command.context = ctx
+ help_embed, _ = await help_embed_creator(ctx.command)
+ view = HelpEmbedView(help_embed, ctx.author)
+ view.message = await ctx.send(embed=error_embed, view=view)
@staticmethod
async def handle_check_failure(ctx: Context, e: errors.CheckFailure) -> None:
diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py
index 9670d42a0..dbc62270b 100644
--- a/tests/bot/exts/backend/test_error_handler.py
+++ b/tests/bot/exts/backend/test_error_handler.py
@@ -414,12 +414,13 @@ class IndividualErrorHandlerTests(unittest.IsolatedAsyncioTestCase):
for case in test_cases:
with self.subTest(error=case["error"], call_prepared=case["call_prepared"]):
self.ctx.reset_mock()
+ self.cog.send_error_with_help = AsyncMock()
self.assertIsNone(await self.cog.handle_user_input_error(self.ctx, case["error"]))
- self.ctx.send.assert_awaited_once()
if case["call_prepared"]:
- self.ctx.send_help.assert_awaited_once()
+ self.cog.send_error_with_help.assert_awaited_once()
else:
- self.ctx.send_help.assert_not_awaited()
+ self.ctx.send.assert_awaited_once()
+ self.cog.send_error_with_help.assert_not_awaited()
async def test_handle_check_failure_errors(self):
"""Should await `ctx.send` when error is check failure."""