diff --git a/docs/autostart_windows.rst b/docs/autostart_windows.rst index 146af533e..a55d8df11 100644 --- a/docs/autostart_windows.rst +++ b/docs/autostart_windows.rst @@ -1,48 +1,48 @@ -.. _autostart_windows: - -============================================== -Setting up auto-restart using batch on Windows -============================================== - -.. note:: This guide assumes that you already have a working Red instance. - ------------------------ -Creating the batch file ------------------------ - -Create a new text document anywhere you want to. This file will be used to launch the bot, so you may want to put it somewhere convenient, like Documents or Desktop. - -Open that document in Notepad, and paste the following text in it: - -.. code-block:: batch - - @ECHO OFF - :RED - CALL "%userprofile%\redenv\Scripts\activate.bat" - python -O -m redbot - - IF %ERRORLEVEL% NEQ 0 ( - ECHO Restarting Red... - GOTO RED - ) - -Replace ```` with the instance name of your bot. -If you created your VENV at a location other than the recommended one, replace ``%userprofile%\redenv\Scripts\activate.bat`` with the path to your VENV. - -Click "File", "Save as". Change the dropdown "Save as type" to "All Files (*.*)". Set the filename to ``start_redbot.bat``, and click save. - -There should now be a new file in the location you created the text document in. You can delete that text document as it is no longer needed. -You can now use the ``start_redbot.bat`` batch file to launch Red by double clicking it. -This script will automatically restart red when the ``[p]restart`` command is used or when the bot shuts down abnormally. - -------------------------- -Launch the bot on startup -------------------------- - -Create a shortcut of your ``start_redbot.bat`` file. - -Open the "Run" dialogue box using Windows Key + R. - -Enter ``shell:startup`` if you want the bot to launch only when the current user logs in, or ``shell:common startup`` if you want the bot to launch when any user logs in. - -Drag the shortcut into the folder that is opened. The bot will now launch on startup. \ No newline at end of file +.. _autostart_windows: + +============================================== +Setting up auto-restart using batch on Windows +============================================== + +.. note:: This guide assumes that you already have a working Red instance. + +----------------------- +Creating the batch file +----------------------- + +Create a new text document anywhere you want to. This file will be used to launch the bot, so you may want to put it somewhere convenient, like Documents or Desktop. + +Open that document in Notepad, and paste the following text in it: + +.. code-block:: batch + + @ECHO OFF + :RED + CALL "%userprofile%\redenv\Scripts\activate.bat" + python -O -m redbot + + IF %ERRORLEVEL% NEQ 0 ( + ECHO Restarting Red... + GOTO RED + ) + +Replace ```` with the instance name of your bot. +If you created your VENV at a location other than the recommended one, replace ``%userprofile%\redenv\Scripts\activate.bat`` with the path to your VENV. + +Click "File", "Save as". Change the dropdown "Save as type" to "All Files (*.*)". Set the filename to ``start_redbot.bat``, and click save. + +There should now be a new file in the location you created the text document in. You can delete that text document as it is no longer needed. +You can now use the ``start_redbot.bat`` batch file to launch Red by double clicking it. +This script will automatically restart red when the ``[p]restart`` command is used or when the bot shuts down abnormally. + +------------------------- +Launch the bot on startup +------------------------- + +Create a shortcut of your ``start_redbot.bat`` file. + +Open the "Run" dialogue box using Windows Key + R. + +Enter ``shell:startup`` if you want the bot to launch only when the current user logs in, or ``shell:common startup`` if you want the bot to launch when any user logs in. + +Drag the shortcut into the folder that is opened. The bot will now launch on startup. diff --git a/docs/framework_tree.rst b/docs/framework_tree.rst index 17a518394..1d989a75b 100644 --- a/docs/framework_tree.rst +++ b/docs/framework_tree.rst @@ -1,21 +1,21 @@ -.. tree module docs - -==== -Tree -==== - -Red uses a subclass of discord.py's ``CommandTree`` object in order to allow Cog Creators to add application commands to their cogs without worrying about the command count limit and to support caching ``AppCommand`` objects. When an app command is added to the bot's tree, it will not show up in ``tree.get_commands`` or other similar methods unless the command is "enabled" with ``[p]slash enable`` (similar to "load"ing a cog) and ``tree.red_check_enabled`` has been run since the command was added to the tree. - -.. note:: - - If you are adding app commands to the tree during load time, the loading process will call ``tree.red_check_enabled`` for your cog and its app commands. If you are adding app commands to the bot **outside of load time**, a call to ``tree.red_check_enabled`` after adding the commands is required to ensure the commands will appear properly. - - If application commands from your cog show up in ``[p]slash list`` as enabled from an ``(unknown)`` cog and disabled from your cog at the same time, you did not follow the instructions above. You must manually call ``tree.red_check_enabled`` **after** adding the commands to the tree. - -.. automodule:: redbot.core.tree - -RedTree -^^^^^^^ - -.. autoclass:: RedTree - :members: +.. tree module docs + +==== +Tree +==== + +Red uses a subclass of discord.py's ``CommandTree`` object in order to allow Cog Creators to add application commands to their cogs without worrying about the command count limit and to support caching ``AppCommand`` objects. When an app command is added to the bot's tree, it will not show up in ``tree.get_commands`` or other similar methods unless the command is "enabled" with ``[p]slash enable`` (similar to "load"ing a cog) and ``tree.red_check_enabled`` has been run since the command was added to the tree. + +.. note:: + + If you are adding app commands to the tree during load time, the loading process will call ``tree.red_check_enabled`` for your cog and its app commands. If you are adding app commands to the bot **outside of load time**, a call to ``tree.red_check_enabled`` after adding the commands is required to ensure the commands will appear properly. + + If application commands from your cog show up in ``[p]slash list`` as enabled from an ``(unknown)`` cog and disabled from your cog at the same time, you did not follow the instructions above. You must manually call ``tree.red_check_enabled`` **after** adding the commands to the tree. + +.. automodule:: redbot.core.tree + +RedTree +^^^^^^^ + +.. autoclass:: RedTree + :members: diff --git a/redbot/core/tree.py b/redbot/core/tree.py index 127a1eed3..01f5853de 100644 --- a/redbot/core/tree.py +++ b/redbot/core/tree.py @@ -1,332 +1,332 @@ -import discord -from discord.abc import Snowflake -from discord.utils import MISSING -from discord.app_commands import ( - Command, - Group, - ContextMenu, - AppCommand, - AppCommandError, - BotMissingPermissions, - CheckFailure, - CommandAlreadyRegistered, - CommandInvokeError, - CommandNotFound, - CommandOnCooldown, - NoPrivateMessage, - TransformerError, -) - -from .i18n import Translator -from .utils.chat_formatting import humanize_list, inline - -import logging -import traceback -from datetime import datetime, timedelta, timezone -from typing import List, Dict, Tuple, Union, Optional, Sequence - - -log = logging.getLogger("red") - -_ = Translator(__name__, __file__) - - -class RedTree(discord.app_commands.CommandTree): - """A container that holds application command information. - - Internally does not actually add commands to the tree unless they are - enabled with ``[p]slash enable``, to support Red's modularity. - See ``discord.app_commands.CommandTree`` for more information. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Same structure as superclass - self._disabled_global_commands: Dict[str, Union[Command, Group]] = {} - self._disabled_context_menus: Dict[Tuple[str, Optional[int], int], ContextMenu] = {} - - def add_command( - self, - command: Union[Command, ContextMenu, Group], - /, - *args, - guild: Optional[Snowflake] = MISSING, - guilds: Sequence[Snowflake] = MISSING, - override: bool = False, - **kwargs, - ) -> None: - """Adds an application command to the tree. - - Commands will be internally stored until enabled by ``[p]slash enable``. - """ - # Allow guild specific commands to bypass the internals for development - if guild is not MISSING or guilds is not MISSING: - return super().add_command( - command, *args, guild=guild, guilds=guilds, override=override, **kwargs - ) - - if isinstance(command, ContextMenu): - name = command.name - type = command.type.value - key = (name, None, type) - - # Handle cases where the command already is in the tree - if not override and key in self._disabled_context_menus: - raise CommandAlreadyRegistered(name, None) - if key in self._context_menus: - if not override: - raise discord.errors.CommandAlreadyRegistered(name, None) - del self._context_menus[key] - - self._disabled_context_menus[key] = command - return - - if not isinstance(command, (Command, Group)): - raise TypeError( - f"Expected an application command, received {command.__class__.__name__} instead" - ) - - root = command.root_parent or command - name = root.name - - # Handle cases where the command already is in the tree - if not override and name in self._disabled_global_commands: - raise discord.errors.CommandAlreadyRegistered(name, None) - if name in self._global_commands: - if not override: - raise discord.errors.CommandAlreadyRegistered(name, None) - del self._global_commands[name] - - self._disabled_global_commands[name] = root - - def remove_command( - self, - command: str, - /, - *args, - guild: Optional[Snowflake] = None, - type: discord.AppCommandType = discord.AppCommandType.chat_input, - **kwargs, - ) -> Optional[Union[Command, ContextMenu, Group]]: - """Removes an application command from this tree.""" - if guild is not None: - return super().remove_command(command, *args, guild=guild, type=type, **kwargs) - if type is discord.AppCommandType.chat_input: - return self._disabled_global_commands.pop(command, None) or super().remove_command( - command, *args, guild=guild, type=type, **kwargs - ) - elif type in (discord.AppCommandType.user, discord.AppCommandType.message): - key = (command, None, type.value) - return self._disabled_context_menus.pop(key, None) or super().remove_command( - command, *args, guild=guild, type=type, **kwargs - ) - - def clear_commands( - self, - *args, - guild: Optional[Snowflake], - type: Optional[discord.AppCommandType] = None, - **kwargs, - ) -> None: - """Clears all application commands from the tree.""" - if guild is not None: - return super().clear_commands(*args, guild=guild, type=type, **kwargs) - - if type is None or type is discord.AppCommandType.chat_input: - self._global_commands.clear() - self._disabled_global_commands.clear() - - if type is None: - self._disabled_context_menus.clear() - else: - self._disabled_context_menus = { - (name, _guild_id, value): cmd - for (name, _guild_id, value), cmd in self._disabled_context_menus.items() - if value != type.value - } - return super().clear_commands(*args, guild=guild, type=type, **kwargs) - - async def sync(self, *args, guild: Optional[Snowflake] = None, **kwargs) -> List[AppCommand]: - """Wrapper to store command IDs when commands are synced.""" - commands = await super().sync(*args, guild=guild, **kwargs) - if guild: - return commands - async with self.client._config.all() as cfg: - for command in commands: - if command.type is discord.AppCommandType.chat_input: - cfg["enabled_slash_commands"][command.name] = command.id - elif command.type is discord.AppCommandType.message: - cfg["enabled_message_commands"][command.name] = command.id - elif command.type is discord.AppCommandType.user: - cfg["enabled_user_commands"][command.name] = command.id - return commands - - async def red_check_enabled(self) -> None: - """Restructures the commands in this tree, enabling commands that are enabled and disabling commands that are disabled. - - After running this function, the tree will be populated with enabled commands only. - If commands are manually added to the tree outside of the standard cog loading process, this must be run - for them to be usable. - """ - enabled_commands = await self.client.list_enabled_app_commands() - - to_add_commands = [] - to_add_context = [] - to_remove_commands = [] - to_remove_context = [] - - # Add commands - for command in enabled_commands["slash"]: - if command in self._disabled_global_commands: - to_add_commands.append(command) - - # Add context - for command in enabled_commands["message"]: - key = (command, None, discord.AppCommandType.message.value) - if key in self._disabled_context_menus: - to_add_context.append(key) - for command in enabled_commands["user"]: - key = (command, None, discord.AppCommandType.user.value) - if key in self._disabled_context_menus: - to_add_context.append(key) - - # Remove commands - for command in self._global_commands: - if command not in enabled_commands["slash"]: - to_remove_commands.append((command, discord.AppCommandType.chat_input)) - - # Remove context - for command, guild_id, command_type in self._context_menus: - if guild_id is not None: - continue - if ( - discord.AppCommandType(command_type) is discord.AppCommandType.message - and command not in enabled_commands["message"] - ): - to_remove_context.append((command, discord.AppCommandType.message)) - elif ( - discord.AppCommandType(command_type) is discord.AppCommandType.user - and command not in enabled_commands["user"] - ): - to_remove_context.append((command, discord.AppCommandType.user)) - - # Actually add/remove - for command in to_add_commands: - super().add_command(self._disabled_global_commands[command]) - del self._disabled_global_commands[command] - for key in to_add_context: - super().add_command(self._disabled_context_menus[key]) - del self._disabled_context_menus[key] - for command, type in to_remove_commands: - com = super().remove_command(command, type=type) - self._disabled_global_commands[command] = com - for command, type in to_remove_context: - com = super().remove_command(command, type=type) - self._disabled_context_menus[(command, None, type.value)] = com - - @staticmethod - async def _send_from_interaction(interaction, *args, **kwargs): - """Util for safely sending a message from an interaction.""" - if interaction.response.is_done(): - if interaction.is_expired(): - return await interaction.channel.send(*args, **kwargs) - return await interaction.followup.send(*args, ephemeral=True, **kwargs) - return await interaction.response.send_message(*args, ephemeral=True, **kwargs) - - @staticmethod - def _is_submodule(parent: str, child: str): - return parent == child or child.startswith(parent + ".") - - async def on_error( - self, interaction: discord.Interaction, error: AppCommandError, /, *args, **kwargs - ) -> None: - """Fallback error handler for app commands.""" - if isinstance(error, CommandNotFound): - await self._send_from_interaction(interaction, _("Command not found.")) - log.warning( - f"Application command {error.name} could not be resolved. " - "It may be from a cog that was updated or unloaded. " - "Consider running [p]slash sync to resolve this issue." - ) - elif isinstance(error, CommandInvokeError): - log.exception( - "Exception in command '{}'".format(error.command.qualified_name), - exc_info=error.original, - ) - exception_log = "Exception in command '{}'\n" "".format(error.command.qualified_name) - exception_log += "".join( - traceback.format_exception(type(error), error, error.__traceback__) - ) - interaction.client._last_exception = exception_log - - message = await interaction.client._config.invoke_error_msg() - if not message: - if interaction.user.id in interaction.client.owner_ids: - message = inline( - _("Error in command '{command}'. Check your console or logs for details.") - ) - else: - message = inline(_("Error in command '{command}'.")) - await self._send_from_interaction( - interaction, message.replace("{command}", error.command.qualified_name) - ) - elif isinstance(error, TransformerError): - if error.__cause__: - log.exception("Error in an app command transformer.", exc_info=error.__cause__) - await self._send_from_interaction(interaction, str(error)) - elif isinstance(error, BotMissingPermissions): - formatted = [ - '"' + perm.replace("_", " ").title() + '"' for perm in error.missing_permissions - ] - formatted = humanize_list(formatted).replace("Guild", "Server") - if len(error.missing_permissions) == 1: - msg = _("I require the {permission} permission to execute that command.").format( - permission=formatted - ) - else: - msg = _("I require {permission_list} permissions to execute that command.").format( - permission_list=formatted - ) - await self._send_from_interaction(interaction, msg) - elif isinstance(error, NoPrivateMessage): - # Seems to be only called normally by the has_role check - await self._send_from_interaction( - interaction, _("That command is not available in DMs.") - ) - elif isinstance(error, CommandOnCooldown): - relative_time = discord.utils.format_dt( - datetime.now(timezone.utc) + timedelta(seconds=error.retry_after), "R" - ) - msg = _("This command is on cooldown. Try again {relative_time}.").format( - relative_time=relative_time - ) - await self._send_from_interaction(interaction, msg, delete_after=error.retry_after) - elif isinstance(error, CheckFailure): - await self._send_from_interaction( - interaction, _("You are not permitted to use this command.") - ) - else: - log.exception(type(error).__name__, exc_info=error) - - # DEP-WARN - def _remove_with_module(self, name: str, *args, **kwargs) -> None: - """Handles cases where a module raises an exception in the loading process, but added commands to the tree. - - Duplication of the logic in the super class, but for the containers used by this subclass. - """ - super()._remove_with_module(name, *args, **kwargs) - remove = [] - for key, cmd in self._disabled_context_menus.items(): - if cmd.module is not None and self._is_submodule(name, cmd.module): - remove.append(key) - - for key in remove: - del self._disabled_context_menus[key] - - remove = [] - for key, cmd in self._disabled_global_commands.items(): - if cmd.module is not None and self._is_submodule(name, cmd.module): - remove.append(key) - - for key in remove: - del self._disabled_global_commands[key] +import discord +from discord.abc import Snowflake +from discord.utils import MISSING +from discord.app_commands import ( + Command, + Group, + ContextMenu, + AppCommand, + AppCommandError, + BotMissingPermissions, + CheckFailure, + CommandAlreadyRegistered, + CommandInvokeError, + CommandNotFound, + CommandOnCooldown, + NoPrivateMessage, + TransformerError, +) + +from .i18n import Translator +from .utils.chat_formatting import humanize_list, inline + +import logging +import traceback +from datetime import datetime, timedelta, timezone +from typing import List, Dict, Tuple, Union, Optional, Sequence + + +log = logging.getLogger("red") + +_ = Translator(__name__, __file__) + + +class RedTree(discord.app_commands.CommandTree): + """A container that holds application command information. + + Internally does not actually add commands to the tree unless they are + enabled with ``[p]slash enable``, to support Red's modularity. + See ``discord.app_commands.CommandTree`` for more information. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Same structure as superclass + self._disabled_global_commands: Dict[str, Union[Command, Group]] = {} + self._disabled_context_menus: Dict[Tuple[str, Optional[int], int], ContextMenu] = {} + + def add_command( + self, + command: Union[Command, ContextMenu, Group], + /, + *args, + guild: Optional[Snowflake] = MISSING, + guilds: Sequence[Snowflake] = MISSING, + override: bool = False, + **kwargs, + ) -> None: + """Adds an application command to the tree. + + Commands will be internally stored until enabled by ``[p]slash enable``. + """ + # Allow guild specific commands to bypass the internals for development + if guild is not MISSING or guilds is not MISSING: + return super().add_command( + command, *args, guild=guild, guilds=guilds, override=override, **kwargs + ) + + if isinstance(command, ContextMenu): + name = command.name + type = command.type.value + key = (name, None, type) + + # Handle cases where the command already is in the tree + if not override and key in self._disabled_context_menus: + raise CommandAlreadyRegistered(name, None) + if key in self._context_menus: + if not override: + raise discord.errors.CommandAlreadyRegistered(name, None) + del self._context_menus[key] + + self._disabled_context_menus[key] = command + return + + if not isinstance(command, (Command, Group)): + raise TypeError( + f"Expected an application command, received {command.__class__.__name__} instead" + ) + + root = command.root_parent or command + name = root.name + + # Handle cases where the command already is in the tree + if not override and name in self._disabled_global_commands: + raise discord.errors.CommandAlreadyRegistered(name, None) + if name in self._global_commands: + if not override: + raise discord.errors.CommandAlreadyRegistered(name, None) + del self._global_commands[name] + + self._disabled_global_commands[name] = root + + def remove_command( + self, + command: str, + /, + *args, + guild: Optional[Snowflake] = None, + type: discord.AppCommandType = discord.AppCommandType.chat_input, + **kwargs, + ) -> Optional[Union[Command, ContextMenu, Group]]: + """Removes an application command from this tree.""" + if guild is not None: + return super().remove_command(command, *args, guild=guild, type=type, **kwargs) + if type is discord.AppCommandType.chat_input: + return self._disabled_global_commands.pop(command, None) or super().remove_command( + command, *args, guild=guild, type=type, **kwargs + ) + elif type in (discord.AppCommandType.user, discord.AppCommandType.message): + key = (command, None, type.value) + return self._disabled_context_menus.pop(key, None) or super().remove_command( + command, *args, guild=guild, type=type, **kwargs + ) + + def clear_commands( + self, + *args, + guild: Optional[Snowflake], + type: Optional[discord.AppCommandType] = None, + **kwargs, + ) -> None: + """Clears all application commands from the tree.""" + if guild is not None: + return super().clear_commands(*args, guild=guild, type=type, **kwargs) + + if type is None or type is discord.AppCommandType.chat_input: + self._global_commands.clear() + self._disabled_global_commands.clear() + + if type is None: + self._disabled_context_menus.clear() + else: + self._disabled_context_menus = { + (name, _guild_id, value): cmd + for (name, _guild_id, value), cmd in self._disabled_context_menus.items() + if value != type.value + } + return super().clear_commands(*args, guild=guild, type=type, **kwargs) + + async def sync(self, *args, guild: Optional[Snowflake] = None, **kwargs) -> List[AppCommand]: + """Wrapper to store command IDs when commands are synced.""" + commands = await super().sync(*args, guild=guild, **kwargs) + if guild: + return commands + async with self.client._config.all() as cfg: + for command in commands: + if command.type is discord.AppCommandType.chat_input: + cfg["enabled_slash_commands"][command.name] = command.id + elif command.type is discord.AppCommandType.message: + cfg["enabled_message_commands"][command.name] = command.id + elif command.type is discord.AppCommandType.user: + cfg["enabled_user_commands"][command.name] = command.id + return commands + + async def red_check_enabled(self) -> None: + """Restructures the commands in this tree, enabling commands that are enabled and disabling commands that are disabled. + + After running this function, the tree will be populated with enabled commands only. + If commands are manually added to the tree outside of the standard cog loading process, this must be run + for them to be usable. + """ + enabled_commands = await self.client.list_enabled_app_commands() + + to_add_commands = [] + to_add_context = [] + to_remove_commands = [] + to_remove_context = [] + + # Add commands + for command in enabled_commands["slash"]: + if command in self._disabled_global_commands: + to_add_commands.append(command) + + # Add context + for command in enabled_commands["message"]: + key = (command, None, discord.AppCommandType.message.value) + if key in self._disabled_context_menus: + to_add_context.append(key) + for command in enabled_commands["user"]: + key = (command, None, discord.AppCommandType.user.value) + if key in self._disabled_context_menus: + to_add_context.append(key) + + # Remove commands + for command in self._global_commands: + if command not in enabled_commands["slash"]: + to_remove_commands.append((command, discord.AppCommandType.chat_input)) + + # Remove context + for command, guild_id, command_type in self._context_menus: + if guild_id is not None: + continue + if ( + discord.AppCommandType(command_type) is discord.AppCommandType.message + and command not in enabled_commands["message"] + ): + to_remove_context.append((command, discord.AppCommandType.message)) + elif ( + discord.AppCommandType(command_type) is discord.AppCommandType.user + and command not in enabled_commands["user"] + ): + to_remove_context.append((command, discord.AppCommandType.user)) + + # Actually add/remove + for command in to_add_commands: + super().add_command(self._disabled_global_commands[command]) + del self._disabled_global_commands[command] + for key in to_add_context: + super().add_command(self._disabled_context_menus[key]) + del self._disabled_context_menus[key] + for command, type in to_remove_commands: + com = super().remove_command(command, type=type) + self._disabled_global_commands[command] = com + for command, type in to_remove_context: + com = super().remove_command(command, type=type) + self._disabled_context_menus[(command, None, type.value)] = com + + @staticmethod + async def _send_from_interaction(interaction, *args, **kwargs): + """Util for safely sending a message from an interaction.""" + if interaction.response.is_done(): + if interaction.is_expired(): + return await interaction.channel.send(*args, **kwargs) + return await interaction.followup.send(*args, ephemeral=True, **kwargs) + return await interaction.response.send_message(*args, ephemeral=True, **kwargs) + + @staticmethod + def _is_submodule(parent: str, child: str): + return parent == child or child.startswith(parent + ".") + + async def on_error( + self, interaction: discord.Interaction, error: AppCommandError, /, *args, **kwargs + ) -> None: + """Fallback error handler for app commands.""" + if isinstance(error, CommandNotFound): + await self._send_from_interaction(interaction, _("Command not found.")) + log.warning( + f"Application command {error.name} could not be resolved. " + "It may be from a cog that was updated or unloaded. " + "Consider running [p]slash sync to resolve this issue." + ) + elif isinstance(error, CommandInvokeError): + log.exception( + "Exception in command '{}'".format(error.command.qualified_name), + exc_info=error.original, + ) + exception_log = "Exception in command '{}'\n" "".format(error.command.qualified_name) + exception_log += "".join( + traceback.format_exception(type(error), error, error.__traceback__) + ) + interaction.client._last_exception = exception_log + + message = await interaction.client._config.invoke_error_msg() + if not message: + if interaction.user.id in interaction.client.owner_ids: + message = inline( + _("Error in command '{command}'. Check your console or logs for details.") + ) + else: + message = inline(_("Error in command '{command}'.")) + await self._send_from_interaction( + interaction, message.replace("{command}", error.command.qualified_name) + ) + elif isinstance(error, TransformerError): + if error.__cause__: + log.exception("Error in an app command transformer.", exc_info=error.__cause__) + await self._send_from_interaction(interaction, str(error)) + elif isinstance(error, BotMissingPermissions): + formatted = [ + '"' + perm.replace("_", " ").title() + '"' for perm in error.missing_permissions + ] + formatted = humanize_list(formatted).replace("Guild", "Server") + if len(error.missing_permissions) == 1: + msg = _("I require the {permission} permission to execute that command.").format( + permission=formatted + ) + else: + msg = _("I require {permission_list} permissions to execute that command.").format( + permission_list=formatted + ) + await self._send_from_interaction(interaction, msg) + elif isinstance(error, NoPrivateMessage): + # Seems to be only called normally by the has_role check + await self._send_from_interaction( + interaction, _("That command is not available in DMs.") + ) + elif isinstance(error, CommandOnCooldown): + relative_time = discord.utils.format_dt( + datetime.now(timezone.utc) + timedelta(seconds=error.retry_after), "R" + ) + msg = _("This command is on cooldown. Try again {relative_time}.").format( + relative_time=relative_time + ) + await self._send_from_interaction(interaction, msg, delete_after=error.retry_after) + elif isinstance(error, CheckFailure): + await self._send_from_interaction( + interaction, _("You are not permitted to use this command.") + ) + else: + log.exception(type(error).__name__, exc_info=error) + + # DEP-WARN + def _remove_with_module(self, name: str, *args, **kwargs) -> None: + """Handles cases where a module raises an exception in the loading process, but added commands to the tree. + + Duplication of the logic in the super class, but for the containers used by this subclass. + """ + super()._remove_with_module(name, *args, **kwargs) + remove = [] + for key, cmd in self._disabled_context_menus.items(): + if cmd.module is not None and self._is_submodule(name, cmd.module): + remove.append(key) + + for key in remove: + del self._disabled_context_menus[key] + + remove = [] + for key, cmd in self._disabled_global_commands.items(): + if cmd.module is not None and self._is_submodule(name, cmd.module): + remove.append(key) + + for key in remove: + del self._disabled_global_commands[key]