-
-
Notifications
You must be signed in to change notification settings - Fork 240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use botcore command error manager #1458
Open
shtlrs
wants to merge
12
commits into
main
Choose a base branch
from
use-botcore-command-error-manager
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+471
−159
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
73ab9d9
implement CommandNotFoundErrorHandler
shtlrs b580fb7
implement DefaultCommandErrorHandler
shtlrs ee3d68c
register default and command not found error handlers
shtlrs 681641c
implement the MovedCommandErrorHandler
shtlrs aad7301
implement the handler for the UserInputError exception
shtlrs 7ec113a
implement the handler for the CommandOnCooldown exception
shtlrs e87a721
implement the handler for the UserNotPlayingError exception
shtlrs 1c07ba0
implement the handler for the ApiError exception
shtlrs a0d5ceb
implement the handler for the BadArgument exception
shtlrs e04a70d
implement the handler for the CheckFailure exception
shtlrs 8a10780
implement the handler for the DisabledCommand exception
shtlrs 6f8a5b1
cleanup the CommandErrorHandler cog
shtlrs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from pydis_core.utils.error_handling.commands import CommandErrorManager | ||
|
||
from bot.bot import Bot | ||
|
||
from .api_error import APIErrorHandler | ||
from .bad_argument import BadArgumentErrorHandler | ||
from .check_failure import CheckFailureErrorHandler | ||
from .command_not_found import CommandNotFoundErrorHandler | ||
from .command_on_cooldown import CommandOnCooldownErrorHandler | ||
from .default import DefaultCommandErrorHandler | ||
from .disabled_command import DisabledCommandErrorHandler | ||
from .moved_command import MovedCommandErrorHandler | ||
from .user_input_error import UserInputErrorHandler | ||
from .user_not_playing import UserNotPlayingErrorHandler | ||
|
||
|
||
def bootstrap_command_error_manager(bot: Bot) -> CommandErrorManager: | ||
"""Bootstraps the command error manager with all the needed error handlers.""" | ||
default_handler = DefaultCommandErrorHandler() | ||
manager = CommandErrorManager(default=default_handler) | ||
manager.register_handler(CommandNotFoundErrorHandler(bot)) | ||
manager.register_handler(MovedCommandErrorHandler()) | ||
manager.register_handler(UserInputErrorHandler()) | ||
manager.register_handler(APIErrorHandler()) | ||
manager.register_handler(CommandOnCooldownErrorHandler()) | ||
manager.register_handler(UserNotPlayingErrorHandler()) | ||
manager.register_handler(BadArgumentErrorHandler()) | ||
manager.register_handler(CheckFailureErrorHandler()) | ||
manager.register_handler(DisabledCommandErrorHandler()) | ||
return manager |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import random | ||
from collections.abc import Iterable | ||
|
||
from discord import Embed, Message | ||
from discord.ext import commands | ||
from pydis_core.utils.logging import get_logger | ||
|
||
from bot.constants import Colours, ERROR_REPLIES | ||
|
||
log = get_logger(__name__) | ||
|
||
|
||
def create_error_embed(message: str, title: Iterable | str = ERROR_REPLIES) -> Embed: | ||
"""Build a basic embed with red colour and either a random error title or a title provided.""" | ||
embed = Embed(colour=Colours.soft_red) | ||
if isinstance(title, str): | ||
embed.title = title | ||
else: | ||
embed.title = random.choice(title) | ||
embed.description = message | ||
return embed | ||
|
||
|
||
def revert_cooldown_counter(command: commands.Command, message: Message) -> None: | ||
"""Undoes the last cooldown counter for user-error cases.""" | ||
if command._buckets.valid: | ||
bucket = command._buckets.get_bucket(message) | ||
bucket._tokens = min(bucket.rate, bucket._tokens + 1) | ||
log.debug("Cooldown counter reverted as the command was not used correctly.") | ||
|
||
|
||
def get_parent_command_and_subcontext(context: commands.Context) -> tuple[str, commands.Context]: | ||
"""Extracts the parent command and subcontext, if any.""" | ||
parent_command = "" | ||
ctx = context | ||
if sub_context := getattr(context, "subcontext", None): | ||
parent_command = f"{context.command} " | ||
ctx = sub_context | ||
|
||
return parent_command, ctx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from typing import NoReturn | ||
|
||
from discord import Interaction | ||
from discord.ext.commands import Context | ||
from pydis_core.utils.error_handling.commands import AbstractCommandErrorHandler | ||
|
||
from bot.constants import NEGATIVE_REPLIES | ||
from bot.utils.exceptions import APIError | ||
|
||
from ._utils import create_error_embed | ||
|
||
|
||
class APIErrorHandler(AbstractCommandErrorHandler): | ||
"""An handler for the APIError error.""" | ||
|
||
async def should_handle_error(self, error: Exception) -> bool: | ||
"""A predicate that determines whether the error should be handled or not.""" | ||
return isinstance(error, APIError) | ||
|
||
async def handle_text_command_error(self, context: Context, error: Exception) -> NoReturn: | ||
"""Handle error raised in the context of text commands.""" | ||
await context.send( | ||
embed=create_error_embed( | ||
f"There was an error when communicating with the {error.api}", | ||
NEGATIVE_REPLIES | ||
) | ||
) | ||
|
||
async def handle_app_command_error(self, interaction: Interaction, error: Exception) -> NoReturn: | ||
"""Handle error raised in the context of app commands.""" | ||
await interaction.response.send_message( | ||
embed=create_error_embed( | ||
f"There was an error when communicating with the {error.api}", | ||
NEGATIVE_REPLIES | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from typing import NoReturn | ||
|
||
from discord import Interaction | ||
from discord.ext.commands import BadArgument, Context | ||
from pydis_core.utils.error_handling.commands import AbstractCommandErrorHandler | ||
|
||
from ._utils import create_error_embed, get_parent_command_and_subcontext, revert_cooldown_counter | ||
|
||
|
||
class BadArgumentErrorHandler(AbstractCommandErrorHandler): | ||
"""An handler for the BadArgument error.""" | ||
|
||
async def should_handle_error(self, error: Exception) -> bool: | ||
"""A predicate that determines whether the error should be handled or not.""" | ||
return isinstance(error, BadArgument) | ||
|
||
async def handle_text_command_error(self, context: Context, error: Exception) -> NoReturn: | ||
"""Handle error raised in the context of text commands.""" | ||
revert_cooldown_counter(context.command, context.message) | ||
parent_command, ctx = get_parent_command_and_subcontext(context) | ||
embed = create_error_embed( | ||
"The argument you provided was invalid: " | ||
f"{error}\n\nUsage:\n```\n{ctx.prefix}{parent_command}{ctx.command} {ctx.command.signature}\n```" | ||
) | ||
await context.send(embed=embed) | ||
|
||
async def handle_app_command_error(self, interaction: Interaction, error: Exception) -> NoReturn: | ||
"""Handle error raised in the context of app commands.""" | ||
return |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from typing import NoReturn | ||
|
||
from discord import Embed, Interaction | ||
from discord.ext.commands import CheckFailure, Context, NoPrivateMessage | ||
from pydis_core.utils.error_handling.commands import AbstractCommandErrorHandler | ||
|
||
from bot.constants import Channels, NEGATIVE_REPLIES | ||
from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure | ||
|
||
from ._utils import create_error_embed | ||
|
||
|
||
class CheckFailureErrorHandler(AbstractCommandErrorHandler): | ||
"""An handler for the CheckFailure error.""" | ||
|
||
async def should_handle_error(self, error: Exception) -> bool: | ||
"""A predicate that determines whether the error should be handled or not.""" | ||
return isinstance(error, CheckFailure) | ||
|
||
async def handle_text_command_error(self, context: Context, error: Exception) -> NoReturn: | ||
"""Handle error raised in the context of text commands.""" | ||
error_embed = self._get_error_embed(error) | ||
await context.send(embed=error_embed, delete_after=7.5) | ||
return | ||
|
||
async def handle_app_command_error(self, interaction: Interaction, error: Exception) -> NoReturn: | ||
"""Handle error raised in the context of app commands.""" | ||
await interaction.response.send_message(embed=self._get_error_embed(error)) | ||
|
||
@staticmethod | ||
def _get_error_embed(error: Exception) -> Embed: | ||
if isinstance(error, InChannelCheckFailure | InMonthCheckFailure): | ||
return create_error_embed(str(error), NEGATIVE_REPLIES) | ||
if isinstance(error, NoPrivateMessage): | ||
return create_error_embed( | ||
"This command can only be used in the server. " | ||
f"Go to <#{Channels.sir_lancebot_playground}> instead!", | ||
NEGATIVE_REPLIES | ||
) | ||
return create_error_embed("You are not authorized to use this command.", NEGATIVE_REPLIES) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
from typing import NoReturn | ||
|
||
from discord import Embed, Interaction, errors | ||
from discord.ext import commands | ||
from pydis_core.utils.error_handling.commands import AbstractCommandErrorHandler | ||
from pydis_core.utils.logging import get_logger | ||
|
||
from bot.bot import Bot | ||
from bot.utils.commands import get_command_suggestions | ||
|
||
log = get_logger(__name__) | ||
|
||
DELETE_DELAY = 10 | ||
QUESTION_MARK_ICON = "https://cdn.discordapp.com/emojis/512367613339369475.png" | ||
|
||
|
||
class CommandNotFoundErrorHandler(AbstractCommandErrorHandler): | ||
"""A handler for all CommandNotFound exceptions.""" | ||
|
||
def __init__(self, bot: Bot): | ||
self.bot = bot | ||
|
||
async def should_handle_error(self, error: errors.DiscordException) -> bool: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thoughts about using |
||
"""A predicate that determines whether the error should be handled or not.""" | ||
return isinstance(error, commands.CommandNotFound) | ||
|
||
async def handle_app_command_error(self, interaction: Interaction, error: errors.DiscordException) -> NoReturn: | ||
"""Handle error raised in the context of app commands.""" | ||
# CommandNotFound cannot happen with app commands, so there's nothing to do here | ||
return | ||
|
||
async def handle_text_command_error(self, context: commands.Context, error: errors.DiscordException) -> NoReturn: | ||
"""Handle error raised in the context of text commands.""" | ||
if not context.invoked_with.startswith("."): | ||
await self.send_command_suggestion(context, context.invoked_with) | ||
|
||
async def send_command_suggestion(self, ctx: commands.Context, command_name: str) -> None: | ||
"""Sends user similar commands if any can be found.""" | ||
command_suggestions = [] | ||
if similar_command_names := get_command_suggestions(list(self.bot.all_commands.keys()), command_name): | ||
for similar_command_name in similar_command_names: | ||
similar_command = self.bot.get_command(similar_command_name) | ||
|
||
if not similar_command: | ||
continue | ||
|
||
log_msg = "Cancelling attempt to suggest a command due to failed checks." | ||
try: | ||
if not await similar_command.can_run(ctx): | ||
log.debug(log_msg) | ||
continue | ||
except commands.errors.CommandError: | ||
log.debug(log_msg) | ||
continue | ||
|
||
command_suggestions.append(similar_command_name) | ||
|
||
misspelled_content = ctx.message.content | ||
e = Embed() | ||
e.set_author(name="Did you mean:", icon_url=QUESTION_MARK_ICON) | ||
e.description = "\n".join( | ||
misspelled_content.replace(command_name, cmd, 1) for cmd in command_suggestions | ||
) | ||
await ctx.send(embed=e, delete_after=DELETE_DELAY) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import math | ||
from typing import NoReturn | ||
|
||
from discord import Interaction | ||
from discord.ext.commands import CommandOnCooldown, Context | ||
from pydis_core.utils.error_handling.commands import AbstractCommandErrorHandler | ||
|
||
from bot.constants import NEGATIVE_REPLIES | ||
|
||
from ._utils import create_error_embed | ||
|
||
|
||
class CommandOnCooldownErrorHandler(AbstractCommandErrorHandler): | ||
"""An handler for the CommandOnCooldown error.""" | ||
|
||
async def should_handle_error(self, error: Exception) -> bool: | ||
"""A predicate that determines whether the error should be handled or not.""" | ||
return isinstance(error, CommandOnCooldown) | ||
|
||
async def handle_text_command_error(self, context: Context, error: Exception) -> NoReturn: | ||
"""Handle error raised in the context of text commands.""" | ||
mins, secs = divmod(math.ceil(error.retry_after), 60) | ||
embed = create_error_embed( | ||
f"This command is on cooldown:\nPlease retry in {mins} minutes {secs} seconds.", | ||
NEGATIVE_REPLIES | ||
) | ||
await context.send(embed=embed, delete_after=7.5) | ||
|
||
async def handle_app_command_error(self, interaction: Interaction, error: Exception) -> NoReturn: | ||
"""Handle error raised in the context of app commands.""" | ||
raise Exception from error |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
from typing import NoReturn | ||
|
||
from discord import Guild, Interaction, errors | ||
from discord.ext.commands import Context | ||
from pydis_core.utils.error_handling.commands import AbstractCommandErrorHandler | ||
from pydis_core.utils.logging import get_logger | ||
from sentry_sdk import push_scope | ||
|
||
log = get_logger(__name__) | ||
|
||
|
||
class DefaultCommandErrorHandler(AbstractCommandErrorHandler): | ||
"""A default command error handler.""" | ||
|
||
async def should_handle_error(self, error: errors.DiscordException) -> bool: | ||
"""A predicate that determines whether the error should be handled or not.""" | ||
return True | ||
|
||
async def handle_text_command_error(self, context: Context, error: errors.DiscordException) -> NoReturn: | ||
"""Handle error raised in the context of text commands.""" | ||
self._handle_unexpected_error( | ||
error=error, | ||
author_id=context.author.id, | ||
username=str(context.author), | ||
command_name=context.command.qualified_name, | ||
message_id=context.message.id, | ||
channel_id=context.channel.id, | ||
content=context.message.content, | ||
guild=context.guild, | ||
jump_url=context.message.jump_url | ||
) | ||
|
||
async def handle_app_command_error(self, interaction: Interaction, error: errors.DiscordException) -> NoReturn: | ||
"""Handle error raised in the context of app commands.""" | ||
self._handle_unexpected_error( | ||
error=error, | ||
author_id=interaction.user.id, | ||
username=str(interaction.user), | ||
command_name=interaction.command.name, | ||
message_id=interaction.message.id, | ||
channel_id=interaction.channel_id, | ||
content=interaction.message.content, | ||
guild=interaction.guild, | ||
jump_url=interaction.message.jump_url | ||
) | ||
|
||
def _handle_unexpected_error( | ||
self, | ||
error: errors.DiscordException, | ||
author_id: int, | ||
username: str, | ||
command_name: str, | ||
message_id: int, | ||
channel_id: int, | ||
content: str, | ||
guild: Guild | None = None, | ||
jump_url: str | None = None | ||
) -> None: | ||
with push_scope() as scope: | ||
scope.user = { | ||
"id": author_id, | ||
"username": username | ||
} | ||
|
||
scope.set_tag("command", command_name) | ||
scope.set_tag("message_id", message_id) | ||
scope.set_tag("channel_id", channel_id) | ||
|
||
scope.set_extra("full_message", content) | ||
|
||
if guild is not None and jump_url is not None: | ||
scope.set_extra("jump_to", jump_url) | ||
|
||
log.exception(f"Unhandled command error: {error!s}", exc_info=error) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default value of a function is
None
. If a function can return, but has no explicit return value, then this annotation should beNone
.NoReturn
means it does not return i.e. control flow of the program is interrupted before it returns. I've only seen this used for functions that always throw an exception, like an abstract method that raises NotImplemented. Also, this was superceded (?) in 3.11 byNever
.