1
Fork 0
mirror of https://github.com/allthingslinux/tux.git synced 2024-10-02 16:43:12 +00:00

feat(ctx_error_handler.py): add new error handler for command context

refactor(error_handler.py): update error handling logic to improve error messages and logging
fix(error_handler.py): handle app command errors separately to avoid conflicts
style(error_handler.py): improve code readability and maintainability by simplifying error handling logic

feat(test_error_handler.py): add new file for testing error handling in discord.py
This new file contains a Cog class with commands to raise every type of discord.py error for testing purposes.

feat: add error handling commands to ErrorTests cog in discord bot

This commit adds a series of commands to the ErrorTests cog in the discord bot. Each command raises a specific exception when invoked, allowing for testing and debugging of error handling mechanisms. The exceptions covered include DisabledCommand, CommandInvokeError, CommandOnCooldown, MaxConcurrencyReached, various ExtensionErrors, ClientException, and CommandRegistrationError.

refactor(error_handler.py): rename ErrorHandler to UnifiedErrorHandler for clarity
feat(error_handler.py): add support for traditional command errors and app command errors
fix(error_handler.py): improve error message for better user experience
style(error_handler.py): refactor code for better readability and maintainability
This commit is contained in:
kzndotsh 2024-05-01 04:10:58 +00:00
parent aa11ece7bc
commit 5e67bc6306
4 changed files with 521 additions and 142 deletions

View file

@ -0,0 +1,63 @@
import contextlib
import sys
import traceback
import discord
from discord.ext import commands
class ContextCommandErrorHandler(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_command_error(self, ctx: commands.Context[commands.Bot], error: Exception) -> None:
"""
Handles errors that occur during command execution.
Args:
ctx: The context in which the error occurred.
error: The exception that was raised.
Returns:
None
Raises:
None
"""
# If the command has its own error handler, or the cog has its own error handler, return
if hasattr(ctx.command, "on_error") or (
ctx.cog and ctx.cog._get_overridden_method(ctx.cog.cog_command_error) is not None
):
return
# Ignore these errors
ignored = (commands.CommandNotFound,)
# Get the original exception if it exists
error = getattr(error, "original", error)
# If the error is in the ignored tuple, return
if isinstance(error, ignored):
return
# If the command has been disabled, send a reply to the user
if isinstance(error, commands.DisabledCommand):
await ctx.send(f"{ctx.command} has been disabled.")
# Private message error
elif isinstance(error, commands.NoPrivateMessage):
with contextlib.suppress(discord.HTTPException):
await ctx.author.send(f"{ctx.command} can not be used in Private Messages.")
# elif isinstance(error, commands.BadArgument):
# if ctx.command and ctx.command.qualified_name == "tag list":
# await ctx.send("I could not find that member. Please try again.")
else:
traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
async def setup(bot: commands.Bot):
await bot.add_cog(ContextCommandErrorHandler(bot))

View file

@ -1,132 +1,98 @@
# utils/error_handler.py
from collections.abc import Callable, Coroutine
import traceback
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import (
BotMissingPermissions,
CommandNotFound,
CommandOnCooldown,
Context,
MissingPermissions,
MissingRequiredArgument,
NotOwner,
)
from utils.tux_logger import TuxLogger
from loguru import logger
logger = TuxLogger(__name__)
ErrorHandlerFunc = Callable[[Context, Exception], Coroutine[None, None, None]]
# Custom error handling mappings and messages.
error_map = {
# app_commands
app_commands.AppCommandError: "An error occurred: {error}",
app_commands.CommandInvokeError: "A command invoke error occurred: {error}",
app_commands.TransformerError: "A transformer error occurred: {error}",
app_commands.MissingRole: "You are missing the role required to use this command.",
app_commands.MissingAnyRole: "You are missing some roles required to use this command.",
app_commands.MissingPermissions: "You are missing the required permissions to use this command.",
app_commands.CheckFailure: "You are not allowed to use this command.",
app_commands.CommandNotFound: "This command was not found.",
app_commands.CommandOnCooldown: "This command is on cooldown. Try again in {error.retry_after:.2f} seconds.",
app_commands.BotMissingPermissions: "The bot is missing the required permissions to use this command.",
app_commands.CommandSignatureMismatch: "The command signature does not match: {error}",
# commands
commands.CommandError: "An error occurred: {error}",
commands.CommandInvokeError: "A command invoke error occurred: {error}",
commands.ConversionError: "An error occurred during conversion: {error}",
commands.MissingRole: "You are missing the role required to use this command.",
commands.MissingAnyRole: "You are missing some roles required to use this command.",
commands.MissingPermissions: "You are missing the required permissions to use this command.",
commands.CheckFailure: "You are not allowed to use this command.",
commands.CommandNotFound: "This command was not found.",
commands.CommandOnCooldown: "This command is on cooldown. Try again in {error.retry_after:.2f} seconds.",
commands.BadArgument: "Invalid argument passed. Correct usage:\n```{ctx.command.usage}```",
commands.MissingRequiredArgument: "Missing required argument. Correct usage:\n```{ctx.command.usage}```",
commands.MissingRequiredAttachment: "Missing required attachment.",
commands.NotOwner: "You are not the owner of this bot.",
commands.BotMissingPermissions: "The bot is missing the required permissions to use this command.",
}
class ErrorHandler(commands.Cog):
def __init__(self, bot):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.error_message = "An error occurred. Please try again later."
bot.tree.error(self.dispatch_to_app_command_handler)
async def send_message_log_error(self, ctx, msg, error, error_type):
"""
Send a message to the context and log the error.
"""
await ctx.send(msg)
logger.error(f"{error_type}: {error}")
async def dispatch_to_app_command_handler(
self, interaction: discord.Interaction, error: app_commands.AppCommandError
):
"""Dispatch command error to app_command_error event."""
await self.on_app_command_error(interaction, error)
async def handle_command_not_found(self, ctx: Context, error):
"""
Handles the case when an invalid command is used.
"""
await self.send_message_log_error(
ctx,
f"I'm sorry, but I couldn't find the command: {ctx.message.content}. Please check your command and try again.",
error,
"CommandNotFound",
)
async def on_app_command_error(
self, interaction: discord.Interaction, error: app_commands.AppCommandError
):
"""Handle app command errors."""
error_message = error_map.get(type(error), self.error_message).format(error=error)
async def handle_missing_permissions(self, ctx: Context, error):
"""
Handles the case when a user does not have the necessary permissions
to use a command.
"""
await self.send_message_log_error(
ctx,
"It seems you're missing the necessary permissions to perform this command.",
f"User '{ctx.author.name}' lacks permission to use this command.",
"MissingPermissions",
)
if interaction.response.is_done():
await interaction.followup.send(error_message, ephemeral=True)
else:
await interaction.response.send_message(error_message, ephemeral=True)
async def handle_bot_missing_permissions(self, ctx: Context, error):
"""
Handles the case when the bot does not have the necessary permissions
to execute a command.
"""
await self.send_message_log_error(
ctx,
"I'm sorry, but I don't have enough permissions to perform this command.",
error,
"BotMissingPermissions",
)
async def handle_command_on_cooldown(self, ctx: Context, error):
"""
Handles the case when a user is on cooldown.
"""
await self.send_message_log_error(
ctx,
"You're on cooldown. Please wait a bit before using this command again.",
error,
"Cooldown",
)
async def handle_missing_required_argument(self, ctx: Context, error):
"""
Handles the case when a user is missing a required argument.
"""
await self.send_message_log_error(
ctx,
"You're missing a required argument. Please check your command and try again.",
error,
"MissingRequiredArgument",
)
async def handle_not_owner(self, ctx: Context, error):
"""
Handles the case when a user is not the owner of the bot.
"""
await self.send_message_log_error(
ctx, "You're not the owner of this bot.", error, "NotOwner"
)
async def handle_other_errors(self, ctx: Context, error):
"""
Handles all other types of errors.
"""
logger.exception(f"Unhandled exception in command {ctx.command}: {error}")
if type(error) not in error_map:
self.log_error_traceback(error)
@commands.Cog.listener()
async def on_command_error(self, ctx: Context, error):
"""Called when an error is raised while invoking a command.
async def on_command_error(
self, ctx: commands.Context[commands.Bot], error: commands.CommandError
):
"""Handle traditional command errors."""
if isinstance(
error,
commands.CommandNotFound
| commands.UnexpectedQuoteError
| commands.InvalidEndOfQuotedStringError
| commands.CheckFailure,
):
return # Ignore these specific errors.
Args:
ctx (Context): The invocation context.
error (Exception): The error that was raised.
error_message = error_map.get(type(error), self.error_message).format(error=error, ctx=ctx)
Note:
This function is called when an error is raised while invoking a command. If the error is not
handled, it will be propagated to the global error handler.
await ctx.send(
content=error_message,
ephemeral=False,
)
https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.on_command_error
"""
error_handlers: dict[type[commands.CommandError], ErrorHandlerFunc] = {
CommandNotFound: self.handle_command_not_found,
MissingPermissions: self.handle_missing_permissions,
BotMissingPermissions: self.handle_bot_missing_permissions,
CommandOnCooldown: self.handle_command_on_cooldown,
MissingRequiredArgument: self.handle_missing_required_argument,
NotOwner: self.handle_not_owner,
}
if type(error) not in error_map:
self.log_error_traceback(error)
handler = error_handlers.get(type(error), self.handle_other_errors)
await handler(ctx, error)
def log_error_traceback(self, error: Exception):
"""Helper method to log error traceback."""
trace = traceback.format_exception(None, error, error.__traceback__)
formatted_trace = "".join(trace)
logger.error(f"Error: {error}\nTraceback:\n{formatted_trace}")
async def setup(bot):
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(ErrorHandler(bot))

View file

@ -0,0 +1,348 @@
"""A Cog dedicated to raise every discord.py error on command with the purpose of testing error handling."""
import discord
from discord.ext import commands
from discord.ext.commands import Context, Bot, Cog
""" Command Errors:
https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#exception-hierarchy
ConversionError
UserInputError
MissingRequiredArgument
TooManyArguments
BadArgument
MessageNotFound
MemberNotFound
UserNotFound
ChannelNotFound
ChannelNotReadable
BadColourArgument
RoleNotFound
BadInviteArgument
EmojiNotFound
PartialEmojiConversionFailure
BadBoolArgument
BadUnionArgument
ArgumentParsingError
CommandNotFound
CheckFailure
BotMissingPermissions
BotMissingRole
BotMissingAnyRole
MissingPermission
MissingRole
MissingAnyRole
CheckAnyFailure
NotOwner
NoPrivateMessage
PrivateMessageOnly
NSFWChannelRequired
DisabledCommand
CommandInvokeError
CommandOnCooldown
MaxConcurrencyReached
"""
class ErrorTests(Cog):
"""For Testing Error Handling"""
def __init__(self, bot: Bot):
self.bot = bot
@discord.ext.commands.group(name="error", aliases=["err"])
async def test_error(self, ctx: Context):
"""Group of commands to raise errors for every type of discord.py error"""
if ctx.invoked_subcommand is None:
# Send the help command for this group
await ctx.send_help(ctx.command)
@test_error.command(name="DiscordException")
async def discord_exception(self, ctx: Context):
"""Base exception class for discord.py"""
# https://discordpy.readthedocs.io/en/latest/api.html#discord.DiscordException
raise discord.DiscordException()
@test_error.command(name="CommandError")
async def CommandError(self, ctx, message=None, *args: object):
"""The base exception type for all command related errors."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.CommandError
raise commands.CommandError(message, *args)
@test_error.command(name="ConversionError")
async def ConversionError(self, ctx, converter, original):
"""Exception raised when a Converter class raises non-CommandError."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ConversionError
raise commands.ConversionError(converter, original)
@test_error.command(name="UserInputError")
async def user_input_error(self, ctx: Context):
"""The base exception type for errors that involve errors regarding user input."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.UserInputError
raise commands.UserInputError()
@test_error.command(name="MissingRequiredArgument")
async def missing_required_argument(self, ctx: Context):
"""Exception raised when parsing a command and a parameter that is required is not encountered."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.MissingRequiredArgument
raise commands.MissingRequiredArgument()
@test_error.command(name="TooManyArguments")
async def too_many_arguments(self, ctx: Context):
"""Exception raised when the command was passed too many arguments and its Command.ignore_extra attribute was not set to True."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.TooManyArguments
raise commands.TooManyArguments()
@test_error.command(name="BadArgument")
async def bad_argument(self, ctx: Context):
"""Exception raised when a parsing or conversion failure is encountered on an argument to pass into a command."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.BadArgument
raise commands.BadArgument()
@test_error.command(name="MessageNotFound")
async def message_not_found(self, ctx: Context):
"""Exception raised when the message provided was not found in the channel."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.MessageNotFound
raise commands.MessageNotFound()
@test_error.command(name="MemberNotFound")
async def member_not_found(self, ctx: Context):
"""Exception raised when the member provided was not found in the bots cache."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.MemberNotFound
raise commands.MemberNotFound()
@test_error.command(name="UserNotFound")
async def user_not_found(self, ctx: Context):
"""Exception raised when the user provided was not found in the bots cache."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.UserNotFound
raise commands.UserNotFound()
@test_error.command(name="ChannelNotFound")
async def channel_not_found(self, ctx: Context):
"""Exception raised when the bot can not find the channel."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ChannelNotFound
raise commands.ChannelNotFound()
@test_error.command(name="ChannelNotReadable")
async def channel_not_readable(self, ctx: Context):
"""Exception raised when the bot does not have permission to read messages in the channel."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ChannelNotReadable
raise commands.ChannelNotReadable()
@test_error.command(name="BadColourArgument")
async def bad_colour_argument(self, ctx: Context):
"""Exception raised when the colour is not valid."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.BadColourArgument
raise commands.BadColourArgument()
@test_error.command(name="RoleNotFound")
async def role_not_found(self, ctx: Context):
"""Exception raised when the bot can not find the role."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.RoleNotFound
raise commands.RoleNotFound()
@test_error.command(name="BadInviteArgument")
async def bad_invite_argument(self, ctx: Context):
"""Exception raised when the invite is invalid or expired."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.BadInviteArgument
raise commands.BadInviteArgument()
@test_error.command(name="EmojiNotFound")
async def emoji_not_found(self, ctx: Context):
"""Exception raised when the bot can not find the emoji."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.EmojiNotFound
raise commands.EmojiNotFound()
@test_error.command(name="PartialEmojiConversionFailure")
async def partial_emoji_conversion_failure(self, ctx: Context):
"""Exception raised when the emoji provided does not match the correct format."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.PartialEmojiConversionFailure
raise commands.PartialEmojiConversionFailure()
@test_error.command(name="BadUnionArgument")
async def bad_union_argument(self, ctx: Context):
"""Exception raised when a typing.Union converter fails for all its associated types."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.BadUnionArgument
raise commands.BadUnionArgument()
@test_error.command(name="ArgumentParsingError")
async def argument_parsing_error(self, ctx: Context):
"""An exception raised when the parser fails to parse a users input."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ArgumentParsingError
raise commands.ArgumentParsingError()
@test_error.command(name="UnexpectedQuoteError")
async def unexpected_quote_error(self, ctx: Context):
"""An exception raised when the parser encounters a quote mark inside a non-quoted string."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.UnexpectedQuoteError
raise commands.UnexpectedQuoteError()
@test_error.command(name="InvalidEndOfQuotedStringError")
async def invalid_end_of_quoted_string_error(self, ctx: Context):
"""An exception raised when a space is expected after the closing quote in a string but a different character is found."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.InvalidEndOfQuotedStringError
raise commands.InvalidEndOfQuotedStringError()
@test_error.command(name="ExpectedClosingQuoteError")
async def expected_closing_quote_error(self, ctx: Context):
"""An exception raised when a quote character is expected but not found."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ExpectedClosingQuoteError
raise commands.ExpectedClosingQuoteError()
@test_error.command(name="CommandNotFound")
async def command_not_found(self, ctx: Context):
"""Exception raised when a command is attempted to be invoked but no command under that name is found."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.CommandNotFound
raise commands.CommandNotFound()
@test_error.command(name="CheckFailure")
async def check_failure(self, ctx: Context):
"""Exception raised when the predicates in Command.checks have failed."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.CheckFailure
raise commands.CheckFailure()
@test_error.command(name="CheckAnyFailure")
async def check_any_failure(self, ctx: Context):
"""Exception raised when all predicates in check_any() fail."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.CheckAnyFailure
raise commands.CheckAnyFailure()
@test_error.command(name="PrivateMessageOnly")
async def private_message_only(self, ctx: Context):
"""Exception raised when an operation does not work outside of private message contexts."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.PrivateMessageOnly
raise commands.PrivateMessageOnly()
@test_error.command(name="NoPrivateMessage")
async def no_private_message(self, ctx: Context):
"""Exception raised when an operation does not work in private message contexts."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.NoPrivateMessage
raise commands.NoPrivateMessage()
@test_error.command(name="NotOwner")
async def not_owner(self, ctx: Context):
"""Exception raised when the message author is not the owner of the bot."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.NotOwner
raise commands.NotOwner()
@test_error.command(name="MissingPermissions")
async def missing_permissions(self, ctx: Context):
"""Exception raised when the command invoker lacks permissions to run a command."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.MissingPermissions
raise commands.MissingPermissions()
@test_error.command(name="BotMissingPermissions")
async def _bot_missing_permissions(self, ctx: Context):
"""Exception raised when the bots member lacks permissions to run a command."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.BotMissingPermissions
raise commands.BotMissingPermissions()
@test_error.command(name="MissingRole")
async def missing_role(self, ctx: Context):
"""Exception raised when the command invoker lacks a role to run a command."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.MissingRole
raise commands.MissingRole()
@test_error.command(name="BotMissingRole")
async def _bot_missing_role(self, ctx: Context):
"""Exception raised when the bots member lacks a role to run a command."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.BotMissingRole
raise commands.BotMissingRole()
@test_error.command(name="MissingAnyRole")
async def missing_any_role(self, ctx: Context):
"""Exception raised when the command invoker lacks any of the roles specified to run a command."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.MissingAnyRole
raise commands.MissingAnyRole()
@test_error.command(name="BotMissingAnyRole")
async def _bot_missing_any_role(self, ctx: Context):
"""Exception raised when the bots member lacks any of the roles specified to run a command."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.BotMissingAnyRole
raise commands.BotMissingAnyRole()
@test_error.command(name="NSFWChannelRequired")
async def nsfw_hannel_required(self, ctx: Context):
"""Exception raised when a channel does not have the required NSFW setting."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.NSFWChannelRequired
raise commands.NSFWChannelRequired()
@test_error.command(name="DisabledCommand")
async def disabled_command(self, ctx: Context):
"""Exception raised when the command being invoked is disabled."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.DisabledCommand
raise commands.DisabledCommand()
@test_error.command(name="CommandInvokeError")
async def command_invoke_error(self, ctx: Context):
"""Exception raised when the command being invoked raised an exception."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.CommandInvokeError
raise commands.CommandInvokeError()
@test_error.command(name="CommandOnCooldown")
async def command_on_cooldown(self, ctx: Context):
"""Exception raised when the command being invoked is on cooldown."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.CommandOnCooldown
raise commands.CommandOnCooldown()
@test_error.command(name="MaxConcurrencyReached")
async def max_concurrency_reached(self, ctx: Context):
"""Exception raised when the command being invoked has reached its maximum concurrency."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.MaxConcurrencyReached
raise commands.MaxConcurrencyReached()
@test_error.command(name="ExtensionError")
async def extension_error(self, ctx: Context):
"""Base exception for extension related errors."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ExtensionError
raise commands.ExtensionError()
@test_error.command(name="ExtensionAlreadyLoaded")
async def extension_already_loaded(self, ctx: Context):
"""An exception raised when an extension has already been loaded."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ExtensionAlreadyLoaded
raise commands.ExtensionAlreadyLoaded()
@test_error.command(name="ExtensionNotLoaded")
async def extension_not_loaded(self, ctx: Context):
"""An exception raised when an extension was not loaded."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ExtensionNotLoaded
raise commands.ExtensionNotLoaded()
@test_error.command(name="NoEntryPointError")
async def no_entry_point_error(self, ctx: Context):
"""An exception raised when an extension does not have a setup entry point function."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.NoEntryPointError
raise commands.NoEntryPointError()
@test_error.command(name="ExtensionFailed")
async def extension_failed(self, ctx: Context):
"""An exception raised when an extension failed to load during execution of the module or setup entry point."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ExtensionFailed
raise commands.ExtensionFailed()
@test_error.command(name="ExtensionNotFound")
async def extension_not_found(self, ctx: Context):
"""An exception raised when an extension is not found."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.ExtensionNotFound
raise commands.ExtensionNotFound()
@test_error.command(name="ClientException")
async def client_exception(self, ctx: Context):
"""Exception thats thrown when an operation in the Client fails."""
# https://discordpy.readthedocs.io/en/latest/api.html#discord.ClientException
raise discord.ClientException()
@test_error.command(name="CommandRegistrationError")
async def command_registration_error(self, ctx: Context):
"""An exception raised when the command cant be added because the name is already taken by a different command."""
# https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.CommandRegistrationError
raise commands.CommandRegistrationError()
async def setup(bot: Bot) -> None:
"""Load the ErrorTests cog."""
await bot.add_cog(ErrorTests(bot))
print("Cog loaded: ErrorTests")

View file

@ -5,8 +5,7 @@ from discord import app_commands
from discord.ext import commands
from loguru import logger
# Custom error handling mappings and messages.
error_map = {
error_map: dict[type[Exception], str] = {
# app_commands
app_commands.AppCommandError: "An error occurred: {error}",
app_commands.CommandInvokeError: "A command invoke error occurred: {error}",
@ -37,29 +36,29 @@ error_map = {
}
class ErrorHandler(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
class UnifiedErrorHandler(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.error_message = "An error occurred. Please try again later."
bot.tree.error(self.dispatch_to_app_command_handler)
self.error_message = "An error occurred. Please try again later or contact support."
# Attach the error handler if using app commands
if hasattr(bot, "tree"):
bot.tree.error(self.dispatch_to_app_command_handler)
async def dispatch_to_app_command_handler(
self, interaction: discord.Interaction, error: app_commands.AppCommandError
):
"""Dispatch command error to app_command_error event."""
await self.on_app_command_error(interaction, error)
"""Dispatch command error to appropriate handler."""
await self.handle_app_command_error(interaction, error)
async def on_app_command_error(
async def handle_app_command_error(
self, interaction: discord.Interaction, error: app_commands.AppCommandError
):
"""Handle app command errors."""
"""Handle errors for app commands."""
error_message = error_map.get(type(error), self.error_message).format(error=error)
if interaction.response.is_done():
await interaction.followup.send(error_message, ephemeral=True)
else:
await interaction.response.send_message(error_message, ephemeral=True)
if type(error) not in error_map:
self.log_error_traceback(error)
@ -67,26 +66,29 @@ class ErrorHandler(commands.Cog):
async def on_command_error(
self, ctx: commands.Context[commands.Bot], error: commands.CommandError
):
"""Handle traditional command errors."""
if isinstance(
error,
commands.CommandNotFound
| commands.UnexpectedQuoteError
| commands.InvalidEndOfQuotedStringError
| commands.CheckFailure,
"""Handle errors for traditional commands."""
if (
hasattr(ctx.command, "on_error")
or ctx.cog
and ctx.cog._get_overridden_method(ctx.cog.cog_command_error) is not None
):
return # Ignore these specific errors.
error_message = error_map.get(type(error), self.error_message).format(error=error, ctx=ctx)
await ctx.send(
content=error_message,
ephemeral=False,
)
return
if isinstance(error, commands.CommandNotFound):
return # Optionally, provide feedback for unknown commands.
error = getattr(error, "original", error)
message: str = self.get_error_message(error, ctx)
await ctx.send(content=message, ephemeral=False)
if type(error) not in error_map:
self.log_error_traceback(error)
def get_error_message(
self, error: Exception, ctx: commands.Context[commands.Bot] | None = None
) -> str:
"""Generate an error message from the error map."""
if ctx:
return error_map.get(type(error), self.error_message).format(error=error, ctx=ctx)
return error_map.get(type(error), self.error_message).format(error=error)
def log_error_traceback(self, error: Exception):
"""Helper method to log error traceback."""
trace = traceback.format_exception(None, error, error.__traceback__)
@ -94,5 +96,5 @@ class ErrorHandler(commands.Cog):
logger.error(f"Error: {error}\nTraceback:\n{formatted_trace}")
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(ErrorHandler(bot))
async def setup(bot: commands.Bot):
await bot.add_cog(UnifiedErrorHandler(bot))