diff --git a/discord/ext/bridge/__init__.py b/discord/ext/bridge/__init__.py deleted file mode 100644 index b92a536e82..0000000000 --- a/discord/ext/bridge/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from .bot import * -from .context import * -from .core import * diff --git a/discord/ext/bridge/bot.py b/discord/ext/bridge/bot.py deleted file mode 100644 index b54ef805cb..0000000000 --- a/discord/ext/bridge/bot.py +++ /dev/null @@ -1,200 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -from abc import ABC -from collections.abc import Iterator - -from discord.commands import ApplicationContext -from discord.errors import CheckFailure, DiscordException -from discord.interactions import Interaction -from discord.message import Message - -from ..commands import AutoShardedBot as ExtAutoShardedBot -from ..commands import Bot as ExtBot -from ..commands import Context as ExtContext -from ..commands import errors -from .context import BridgeApplicationContext, BridgeExtContext -from .core import ( - BridgeCommand, - BridgeCommandGroup, - BridgeExtCommand, - BridgeSlashCommand, - bridge_command, - bridge_group, -) - -__all__ = ("Bot", "AutoShardedBot") - - -class BotBase(ABC): - _bridge_commands: list[BridgeCommand | BridgeCommandGroup] - - @property - def bridge_commands(self) -> list[BridgeCommand | BridgeCommandGroup]: - """Returns all of the bot's bridge commands.""" - - if not (cmds := getattr(self, "_bridge_commands", None)): - self._bridge_commands = cmds = [] - - return cmds - - def walk_bridge_commands( - self, - ) -> Iterator[BridgeCommand | BridgeCommandGroup]: - """An iterator that recursively walks through all the bot's bridge commands. - - Yields - ------ - Union[:class:`.BridgeCommand`, :class:`.BridgeCommandGroup`] - A bridge command or bridge group of the bot. - """ - for cmd in self._bridge_commands: - yield cmd - if isinstance(cmd, BridgeCommandGroup): - yield from cmd.walk_commands() - - async def get_application_context(self, interaction: Interaction, cls=None) -> BridgeApplicationContext: - cls = cls if cls is not None else BridgeApplicationContext - # Ignore the type hinting error here. BridgeApplicationContext is a subclass of ApplicationContext, and since - # we gave it cls, it will be used instead. - return await super().get_application_context(interaction, cls=cls) # type: ignore - - async def get_context(self, message: Message, cls=None) -> BridgeExtContext: - cls = cls if cls is not None else BridgeExtContext - # Ignore the type hinting error here. BridgeExtContext is a subclass of Context, and since we gave it cls, it - # will be used instead. - return await super().get_context(message, cls=cls) # type: ignore - - def add_bridge_command(self, command: BridgeCommand): - """Takes a :class:`.BridgeCommand` and adds both a slash and traditional (prefix-based) version of the command - to the bot. - """ - # Ignore the type hinting error here. All subclasses of BotBase pass the type checks. - command.add_to(self) # type: ignore - - self.bridge_commands.append(command) - - def bridge_command(self, **kwargs): - """A shortcut decorator that invokes :func:`bridge_command` and adds it to - the internal command list via :meth:`~.Bot.add_bridge_command`. - - Returns - ------- - Callable[..., :class:`BridgeCommand`] - A decorator that converts the provided method into an :class:`.BridgeCommand`, adds both a slash and - traditional (prefix-based) version of the command to the bot, and returns the :class:`.BridgeCommand`. - """ - - def decorator(func) -> BridgeCommand: - result = bridge_command(**kwargs)(func) - self.add_bridge_command(result) - return result - - return decorator - - def bridge_group(self, **kwargs): - """A decorator that is used to wrap a function as a bridge command group. - - Parameters - ---------- - kwargs: Optional[Dict[:class:`str`, Any]] - Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommandGroup` and :class:`.ext.commands.Group`) - """ - - def decorator(func) -> BridgeCommandGroup: - result = bridge_group(**kwargs)(func) - self.add_bridge_command(result) - return result - - return decorator - - async def invoke(self, ctx: ExtContext | BridgeExtContext): - if ctx.command is not None: - self.dispatch("command", ctx) - if isinstance(ctx.command, BridgeExtCommand): - self.dispatch("bridge_command", ctx) - try: - if await self.can_run(ctx, call_once=True): - await ctx.command.invoke(ctx) - else: - raise errors.CheckFailure("The global check once functions failed.") - except errors.CommandError as exc: - await ctx.command.dispatch_error(ctx, exc) - else: - self.dispatch("command_completion", ctx) - if isinstance(ctx.command, BridgeExtCommand): - self.dispatch("bridge_command_completion", ctx) - elif ctx.invoked_with: - exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found') - self.dispatch("command_error", ctx, exc) - if isinstance(ctx.command, BridgeExtCommand): - self.dispatch("bridge_command_error", ctx, exc) - - async def invoke_application_command(self, ctx: ApplicationContext | BridgeApplicationContext) -> None: - """|coro| - - Invokes the application command given under the invocation - context and handles all the internal event dispatch mechanisms. - - Parameters - ---------- - ctx: :class:`.ApplicationCommand` - The invocation context to invoke. - """ - self._bot.dispatch("application_command", ctx) - if br_cmd := isinstance(ctx.command, BridgeSlashCommand): - self._bot.dispatch("bridge_command", ctx) - try: - if await self._bot.can_run(ctx, call_once=True): - await ctx.command.invoke(ctx) - else: - raise CheckFailure("The global check once functions failed.") - except DiscordException as exc: - await ctx.command.dispatch_error(ctx, exc) - else: - self._bot.dispatch("application_command_completion", ctx) - if br_cmd: - self._bot.dispatch("bridge_command_completion", ctx) - - -class Bot(BotBase, ExtBot): - """Represents a discord bot, with support for cross-compatibility between command types. - - This class is a subclass of :class:`.ext.commands.Bot` and as a result - anything that you can do with a :class:`.ext.commands.Bot` you can do with - this bot. - - .. versionadded:: 2.0 - """ - - -class AutoShardedBot(BotBase, ExtAutoShardedBot): - """This is similar to :class:`.Bot` except that it is inherited from - :class:`.ext.commands.AutoShardedBot` instead. - - .. versionadded:: 2.0 - """ diff --git a/discord/ext/bridge/context.py b/discord/ext/bridge/context.py deleted file mode 100644 index 331ff2a900..0000000000 --- a/discord/ext/bridge/context.py +++ /dev/null @@ -1,197 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Union, overload - -from discord.commands import ApplicationContext -from discord.interactions import Interaction, InteractionMessage -from discord.message import Message -from discord.webhook import WebhookMessage - -from ..commands import Context - -if TYPE_CHECKING: - from .core import BridgeExtCommand, BridgeSlashCommand - - -__all__ = ("BridgeContext", "BridgeExtContext", "BridgeApplicationContext", "Context") - - -class BridgeContext(ABC): - """ - The base context class for compatibility commands. This class is an :term:`abstract base class` (also known as an - ``abc``), which is subclassed by :class:`BridgeExtContext` and :class:`BridgeApplicationContext`. The methods in - this class are meant to give parity between the two contexts, while still allowing for all of their functionality. - - When this is passed to a command, it will either be passed as :class:`BridgeExtContext`, or - :class:`BridgeApplicationContext`. Since they are two separate classes, it's easy to use the :attr:`BridgeContext.is_app` attribute. - to make different functionality for each context. For example, if you want to respond to a command with the command - type that it was invoked with, you can do the following: - - .. code-block:: python3 - - @bot.bridge_command() - async def example(ctx: BridgeContext): - if ctx.is_app: - command_type = "Application command" - else: - command_type = "Traditional (prefix-based) command" - await ctx.send(f"This command was invoked with a(n) {command_type}.") - - .. versionadded:: 2.0 - """ - - @abstractmethod - async def _respond(self, *args, **kwargs) -> Interaction | WebhookMessage | Message: ... - - @abstractmethod - async def _defer(self, *args, **kwargs) -> None: ... - - @abstractmethod - async def _edit(self, *args, **kwargs) -> InteractionMessage | Message: ... - - @overload - async def invoke(self, command: BridgeSlashCommand | BridgeExtCommand, *args, **kwargs) -> None: ... - - async def respond(self, *args, **kwargs) -> Interaction | WebhookMessage | Message: - """|coro| - - Responds to the command with the respective response type to the current context. In :class:`BridgeExtContext`, - this will be :meth:`~.Context.reply` while in :class:`BridgeApplicationContext`, this will be - :meth:`~.ApplicationContext.respond`. - """ - return await self._respond(*args, **kwargs) - - async def reply(self, *args, **kwargs) -> Interaction | WebhookMessage | Message: - """|coro| - - Alias for :meth:`~.BridgeContext.respond`. - """ - return await self.respond(*args, **kwargs) - - async def defer(self, *args, **kwargs) -> None: - """|coro| - - Defers the command with the respective approach to the current context. In :class:`BridgeExtContext`, this will - be :meth:`~discord.abc.Messageable.trigger_typing` while in :class:`BridgeApplicationContext`, this will be - :attr:`~.ApplicationContext.defer`. - - .. note:: - There is no ``trigger_typing`` alias for this method. ``trigger_typing`` will always provide the same - functionality across contexts. - """ - return await self._defer(*args, **kwargs) - - async def edit(self, *args, **kwargs) -> InteractionMessage | Message: - """|coro| - - Edits the original response message with the respective approach to the current context. In - :class:`BridgeExtContext`, this will have a custom approach where :meth:`.respond` caches the message to be - edited here. In :class:`BridgeApplicationContext`, this will be :attr:`~.ApplicationContext.edit`. - """ - return await self._edit(*args, **kwargs) - - def _get_super(self, attr: str) -> Any: - return getattr(super(), attr) - - @property - def is_app(self) -> bool: - """Whether the context is an :class:`BridgeApplicationContext` or not.""" - return isinstance(self, BridgeApplicationContext) - - -class BridgeApplicationContext(BridgeContext, ApplicationContext): - """ - The application context class for compatibility commands. This class is a subclass of :class:`BridgeContext` and - :class:`~.ApplicationContext`. This class is meant to be used with :class:`BridgeCommand`. - - .. versionadded:: 2.0 - """ - - def __init__(self, *args, **kwargs): - # This is needed in order to represent the correct class init signature on the docs - super().__init__(*args, **kwargs) - - async def _respond(self, *args, **kwargs) -> Interaction | WebhookMessage: - return await self._get_super("respond")(*args, **kwargs) - - async def _defer(self, *args, **kwargs) -> None: - return await self._get_super("defer")(*args, **kwargs) - - async def _edit(self, *args, **kwargs) -> InteractionMessage: - return await self._get_super("edit")(*args, **kwargs) - - -class BridgeExtContext(BridgeContext, Context): - """ - The ext.commands context class for compatibility commands. This class is a subclass of :class:`BridgeContext` and - :class:`~.Context`. This class is meant to be used with :class:`BridgeCommand`. - - .. versionadded:: 2.0 - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._original_response_message: Message | None = None - - async def _respond(self, *args, **kwargs) -> Message: - kwargs.pop("ephemeral", None) - message = await self._get_super("reply")(*args, **kwargs) - if self._original_response_message is None: - self._original_response_message = message - return message - - async def _defer(self, *args, **kwargs) -> None: - kwargs.pop("ephemeral", None) - return await self._get_super("trigger_typing")(*args, **kwargs) - - async def _edit(self, *args, **kwargs) -> Message | None: - if self._original_response_message: - return await self._original_response_message.edit(*args, **kwargs) - - async def delete(self, *, delay: float | None = None, reason: str | None = None) -> None: - """|coro| - - Deletes the original response message, if it exists. - - Parameters - ---------- - delay: Optional[:class:`float`] - If provided, the number of seconds to wait before deleting the message. - reason: Optional[:class:`str`] - The reason for deleting the message. Shows up on the audit log. - """ - if self._original_response_message: - await self._original_response_message.delete(delay=delay, reason=reason) - - -Context = BridgeExtContext | BridgeApplicationContext -""" -A Union class for either :class:`BridgeExtContext` or :class:`BridgeApplicationContext`. -Can be used as a type hint for Context for bridge commands. -""" diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py deleted file mode 100644 index d2f2549f93..0000000000 --- a/discord/ext/bridge/core.py +++ /dev/null @@ -1,666 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -import inspect -from collections.abc import Iterator -from typing import TYPE_CHECKING, Any, Callable - -import discord.commands.options -from discord import ( - ApplicationCommand, - Attachment, - Option, - Permissions, - SlashCommand, - SlashCommandGroup, - SlashCommandOptionType, -) -from discord.utils import MISSING, find -from discord.utils.private import warn_deprecated - -from ..commands import ( - BadArgument, - Command, - Context, - Converter, - Group, - GuildChannelConverter, - MemberConverter, - RoleConverter, - UserConverter, -) -from ..commands import Bot as ExtBot -from ..commands.converter import _convert_to_bool, run_converters - -if TYPE_CHECKING: - from .context import BridgeApplicationContext, BridgeExtContext - - -__all__ = ( - "BridgeCommand", - "BridgeCommandGroup", - "bridge_command", - "bridge_group", - "bridge_option", - "BridgeExtCommand", - "BridgeSlashCommand", - "BridgeExtGroup", - "BridgeSlashGroup", - "BridgeOption", - "map_to", - "guild_only", - "has_permissions", - "is_nsfw", -) - - -class BridgeSlashCommand(SlashCommand): - """A subclass of :class:`.SlashCommand` that is used for bridge commands.""" - - def __init__(self, func, **kwargs): - self.brief = kwargs.pop("brief", None) - super().__init__(func, **kwargs) - - async def dispatch_error(self, ctx: BridgeApplicationContext, error: Exception) -> None: - await super().dispatch_error(ctx, error) - ctx.bot.dispatch("bridge_command_error", ctx, error) - - -class BridgeExtCommand(Command): - """A subclass of :class:`.ext.commands.Command` that is used for bridge commands.""" - - def __init__(self, func, **kwargs): - super().__init__(func, **kwargs) - - for option in self.params.values(): - if isinstance(option.annotation, Option) and not isinstance(option.annotation, BridgeOption): - raise TypeError( - f"{option.annotation.__class__.__name__} is not supported in bridge commands. Use BridgeOption instead." - ) - - async def dispatch_error(self, ctx: BridgeExtContext, error: Exception) -> None: - await super().dispatch_error(ctx, error) - ctx.bot.dispatch("bridge_command_error", ctx, error) - - async def transform(self, ctx: Context, param: inspect.Parameter) -> Any: - if param.annotation is Attachment: - # skip the parameter checks for bridge attachments - return await run_converters(ctx, AttachmentConverter, None, param) - else: - return await super().transform(ctx, param) - - -class BridgeSlashGroup(SlashCommandGroup): - """A subclass of :class:`.SlashCommandGroup` that is used for bridge commands.""" - - __slots__ = ("module",) - - def __init__(self, callback, *args, **kwargs): - if perms := getattr(callback, "__default_member_permissions__", None): - kwargs["default_member_permissions"] = perms - super().__init__(*args, **kwargs) - self.callback = callback - self.__original_kwargs__["callback"] = callback - self.__command = None - - async def _invoke(self, ctx: BridgeApplicationContext) -> None: - if not (options := ctx.interaction.data.get("options")): - if not self.__command: - self.__command = BridgeSlashCommand(self.callback) - ctx.command = self.__command - return await ctx.command.invoke(ctx) - option = options[0] - resolved = ctx.interaction.data.get("resolved", None) - command = find(lambda x: x.name == option["name"], self.subcommands) - option["resolved"] = resolved - ctx.interaction.data = option - await command.invoke(ctx) - - -class BridgeExtGroup(BridgeExtCommand, Group): - """A subclass of :class:`.ext.commands.Group` that is used for bridge commands.""" - - -class BridgeCommand: - """Compatibility class between prefixed-based commands and slash commands. - - Parameters - ---------- - callback: Callable[[:class:`.BridgeContext`, ...], Awaitable[Any]] - The callback to invoke when the command is executed. The first argument will be a :class:`BridgeContext`, - and any additional arguments will be passed to the callback. This callback must be a coroutine. - parent: Optional[:class:`.BridgeCommandGroup`]: - Parent of the BridgeCommand. - kwargs: Optional[Dict[:class:`str`, Any]] - Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommand` and :class:`.ext.commands.Command`) - - Attributes - ---------- - slash_variant: :class:`.BridgeSlashCommand` - The slash command version of this bridge command. - ext_variant: :class:`.BridgeExtCommand` - The prefix-based version of this bridge command. - """ - - __bridge__: bool = True - - __special_attrs__ = ["slash_variant", "ext_variant", "parent"] - - def __init__(self, callback, **kwargs): - self.parent = kwargs.pop("parent", None) - self.slash_variant: BridgeSlashCommand = kwargs.pop("slash_variant", None) or BridgeSlashCommand( - callback, **kwargs - ) - self.ext_variant: BridgeExtCommand = kwargs.pop("ext_variant", None) or BridgeExtCommand(callback, **kwargs) - - @property - def name_localizations(self) -> dict[str, str] | None: - """Returns name_localizations from :attr:`slash_variant` - You can edit/set name_localizations directly with - - .. code-block:: python3 - - bridge_command.name_localizations["en-UK"] = ... # or any other locale - # or - bridge_command.name_localizations = {"en-UK": ..., "fr-FR": ...} - """ - return self.slash_variant.name_localizations - - @name_localizations.setter - def name_localizations(self, value): - self.slash_variant.name_localizations = value - - @property - def description_localizations(self) -> dict[str, str] | None: - """Returns description_localizations from :attr:`slash_variant` - You can edit/set description_localizations directly with - - .. code-block:: python3 - - bridge_command.description_localizations["en-UK"] = ... # or any other locale - # or - bridge_command.description_localizations = {"en-UK": ..., "fr-FR": ...} - """ - return self.slash_variant.description_localizations - - @description_localizations.setter - def description_localizations(self, value): - self.slash_variant.description_localizations = value - - def __getattribute__(self, name): - try: - # first, look for the attribute on the bridge command - return super().__getattribute__(name) - except AttributeError as e: - # if it doesn't exist, check this list, if the name of - # the parameter is here - if name in self.__special_attrs__: - raise e - - # looks up the result in the variants. - # slash cmd prioritized - result = getattr(self.slash_variant, name, MISSING) - try: - if result is MISSING: - return getattr(self.ext_variant, name) - return result - except AttributeError as e: - raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") from e - - def __setattr__(self, name, value) -> None: - if name not in self.__special_attrs__: - setattr(self.slash_variant, name, value) - setattr(self.ext_variant, name, value) - - return super().__setattr__(name, value) - - def add_to(self, bot: ExtBot) -> None: - """Adds the command to a bot. This method is inherited by :class:`.BridgeCommandGroup`. - - Parameters - ---------- - bot: Union[:class:`.Bot`, :class:`.AutoShardedBot`] - The bot to add the command to. - """ - bot.add_application_command(self.slash_variant) - bot.add_command(self.ext_variant) - - async def invoke(self, ctx: BridgeExtContext | BridgeApplicationContext, /, *args, **kwargs): - if ctx.is_app: - return await self.slash_variant.invoke(ctx) - return await self.ext_variant.invoke(ctx) - - def error(self, coro): - """A decorator that registers a coroutine as a local error handler. - - This error handler is limited to the command it is defined to. - However, higher scope handlers (per-cog and global) are still - invoked afterwards as a catch-all. This handler also functions as - the handler for both the prefixed and slash versions of the command. - - This error handler takes two parameters, a :class:`.BridgeContext` and - a :class:`~discord.DiscordException`. - - Parameters - ---------- - coro: :ref:`coroutine ` - The coroutine to register as the local error handler. - - Raises - ------ - TypeError - The coroutine passed is not actually a coroutine. - """ - self.slash_variant.error(coro) - self.ext_variant.on_error = coro - - return coro - - def before_invoke(self, coro): - """A decorator that registers a coroutine as a pre-invoke hook. - - This hook is called directly before the command is called, making - it useful for any sort of set up required. This hook is called - for both the prefixed and slash versions of the command. - - This pre-invoke hook takes a sole parameter, a :class:`.BridgeContext`. - - Parameters - ---------- - coro: :ref:`coroutine ` - The coroutine to register as the pre-invoke hook. - - Raises - ------ - TypeError - The coroutine passed is not actually a coroutine. - """ - self.slash_variant.before_invoke(coro) - self.ext_variant._before_invoke = coro - - return coro - - def after_invoke(self, coro): - """A decorator that registers a coroutine as a post-invoke hook. - - This hook is called directly after the command is called, making it - useful for any sort of clean up required. This hook is called for - both the prefixed and slash versions of the command. - - This post-invoke hook takes a sole parameter, a :class:`.BridgeContext`. - - Parameters - ---------- - coro: :ref:`coroutine ` - The coroutine to register as the post-invoke hook. - - Raises - ------ - TypeError - The coroutine passed is not actually a coroutine. - """ - self.slash_variant.after_invoke(coro) - self.ext_variant._after_invoke = coro - - return coro - - -class BridgeCommandGroup(BridgeCommand): - """Compatibility class between prefixed-based commands and slash commands. - - Parameters - ---------- - callback: Callable[[:class:`.BridgeContext`, ...], Awaitable[Any]] - The callback to invoke when the command is executed. The first argument will be a :class:`BridgeContext`, - and any additional arguments will be passed to the callback. This callback must be a coroutine. - kwargs: Optional[Dict[:class:`str`, Any]] - Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommand` and :class:`.ext.commands.Command`) - - Attributes - ---------- - slash_variant: :class:`.SlashCommandGroup` - The slash command version of this command group. - ext_variant: :class:`.ext.commands.Group` - The prefix-based version of this command group. - subcommands: List[:class:`.BridgeCommand`] - List of bridge commands in this group - mapped: Optional[:class:`.SlashCommand`] - If :func:`map_to` is used, the mapped slash command. - """ - - __special_attrs__ = [ - "slash_variant", - "ext_variant", - "parent", - "subcommands", - "mapped", - ] - - ext_variant: BridgeExtGroup - slash_variant: BridgeSlashGroup - - def __init__(self, callback, *args, **kwargs): - ext_var = BridgeExtGroup(callback, *args, **kwargs) - kwargs.update({"name": ext_var.name}) - super().__init__( - callback, - ext_variant=ext_var, - slash_variant=BridgeSlashGroup(callback, *args, **kwargs), - parent=kwargs.pop("parent", None), - ) - - self.subcommands: list[BridgeCommand] = [] - - self.mapped: SlashCommand | None = None - if map_to := getattr(callback, "__custom_map_to__", None): - kwargs.update(map_to) - self.mapped = self.slash_variant.command(**kwargs)(callback) - - def walk_commands(self) -> Iterator[BridgeCommand]: - """An iterator that recursively walks through all the bridge group's subcommands. - - Yields - ------ - :class:`.BridgeCommand` - A bridge command of this bridge group. - """ - yield from self.subcommands - - def command(self, *args, **kwargs): - """A decorator to register a function as a subcommand. - - Parameters - ---------- - kwargs: Optional[Dict[:class:`str`, Any]] - Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommand` and :class:`.ext.commands.Command`) - """ - - def wrap(callback): - slash = self.slash_variant.command( - *args, - **kwargs, - cls=BridgeSlashCommand, - )(callback) - ext = self.ext_variant.command( - *args, - **kwargs, - cls=BridgeExtCommand, - )(callback) - command = BridgeCommand(callback, parent=self, slash_variant=slash, ext_variant=ext) - self.subcommands.append(command) - return command - - return wrap - - -def bridge_command(**kwargs): - """A decorator that is used to wrap a function as a bridge command. - - Parameters - ---------- - kwargs: Optional[Dict[:class:`str`, Any]] - Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommand` and :class:`.ext.commands.Command`) - """ - - def decorator(callback): - return BridgeCommand(callback, **kwargs) - - return decorator - - -def bridge_group(**kwargs): - """A decorator that is used to wrap a function as a bridge command group. - - Parameters - ---------- - kwargs: Optional[Dict[:class:`str`, Any]] - Keyword arguments that are directly passed to the respective command constructors (:class:`.SlashCommandGroup` and :class:`.ext.commands.Group`). - """ - - def decorator(callback): - return BridgeCommandGroup(callback, **kwargs) - - return decorator - - -def map_to(name, description=None): - """To be used with bridge command groups, map the main command to a slash subcommand. - - Parameters - ---------- - name: :class:`str` - The new name of the mapped command. - description: Optional[:class:`str`] - The new description of the mapped command. - - Example - ------- - - .. code-block:: python3 - - @bot.bridge_group() - @bridge.map_to("show") - async def config(ctx: BridgeContext): ... - - - @config.command() - async def toggle(ctx: BridgeContext): ... - - Prefixed commands will not be affected, but slash commands will appear as: - - .. code-block:: - - /config show - /config toggle - """ - - def decorator(callback): - callback.__custom_map_to__ = {"name": name, "description": description} - return callback - - return decorator - - -def guild_only(): - """Intended to work with :class:`.ApplicationCommand` and :class:`BridgeCommand`, adds a :func:`~ext.commands.check` - that locks the command to only run in guilds, and also registers the command as guild only client-side (on discord). - - Basically a utility function that wraps both :func:`discord.ext.commands.guild_only` and :func:`discord.commands.guild_only`. - """ - - def predicate(func: Callable | ApplicationCommand): - if isinstance(func, ApplicationCommand): - func.guild_only = True - else: - func.__guild_only__ = True - - from ..commands import guild_only - - return guild_only()(func) - - return predicate - - -def is_nsfw(): - """Intended to work with :class:`.ApplicationCommand` and :class:`BridgeCommand`, adds a :func:`~ext.commands.check` - that locks the command to only run in nsfw contexts, and also registers the command as nsfw client-side (on discord). - - Basically a utility function that wraps both :func:`discord.ext.commands.is_nsfw` and :func:`discord.commands.is_nsfw`. - - .. warning:: - - In DMs, the prefixed-based command will always run as the user's privacy settings cannot be checked directly. - """ - - def predicate(func: Callable | ApplicationCommand): - if isinstance(func, ApplicationCommand): - func.nsfw = True - else: - func.__nsfw__ = True - - from ..commands import is_nsfw - - return is_nsfw()(func) - - return predicate - - -def has_permissions(**perms: bool): - r"""Intended to work with :class:`.SlashCommand` and :class:`BridgeCommand`, adds a - :func:`~ext.commands.check` that locks the command to be run by people with certain - permissions inside guilds, and also registers the command as locked behind said permissions. - - Basically a utility function that wraps both :func:`discord.ext.commands.has_permissions` - and :func:`discord.commands.default_permissions`. - - Parameters - ---------- - \*\*perms: Dict[:class:`str`, :class:`bool`] - An argument list of permissions to check for. - """ - - def predicate(func: Callable | ApplicationCommand): - from ..commands import has_permissions - - func = has_permissions(**perms)(func) - _perms = Permissions(**perms) - if isinstance(func, ApplicationCommand): - func.default_member_permissions = _perms - else: - func.__default_member_permissions__ = _perms - - return func - - return predicate - - -class MentionableConverter(Converter): - """A converter that can convert a mention to a member, a user or a role.""" - - async def convert(self, ctx, argument): - try: - return await RoleConverter().convert(ctx, argument) - except BadArgument: - pass - - if ctx.guild: - try: - return await MemberConverter().convert(ctx, argument) - except BadArgument: - pass - - return await UserConverter().convert(ctx, argument) - - -class AttachmentConverter(Converter): - async def convert(self, ctx: Context, arg: str): - try: - attach = ctx.message.attachments[0] - except IndexError as e: - raise BadArgument("At least 1 attachment is needed") from e - else: - return attach - - -class BooleanConverter(Converter): - async def convert(self, ctx, arg: bool): - return _convert_to_bool(str(arg)) - - -BRIDGE_CONVERTER_MAPPING = { - SlashCommandOptionType.string: str, - SlashCommandOptionType.integer: int, - SlashCommandOptionType.boolean: BooleanConverter, - SlashCommandOptionType.user: UserConverter, - SlashCommandOptionType.channel: GuildChannelConverter, - SlashCommandOptionType.role: RoleConverter, - SlashCommandOptionType.mentionable: MentionableConverter, - SlashCommandOptionType.number: float, - SlashCommandOptionType.attachment: AttachmentConverter, - discord.Member: MemberConverter, -} - - -class BridgeOption(Option, Converter): - """A subclass of :class:`discord.Option` which represents a selectable slash - command option and a prefixed command argument for bridge commands. - """ - - def __init__(self, input_type, *args, **kwargs): - self.converter = kwargs.pop("converter", None) - super().__init__(input_type, *args, **kwargs) - - self.converter = self.converter or BRIDGE_CONVERTER_MAPPING.get(input_type) - - async def convert(self, ctx, argument: str) -> Any: - try: - if self.converter is not None: - converted = await self.converter().convert(ctx, argument) - else: - converter = BRIDGE_CONVERTER_MAPPING.get(self.input_type) - if isinstance(converter, type) and issubclass(converter, Converter): - converted = await converter().convert(ctx, argument) # type: ignore # protocol class - elif callable(converter): - converted = converter(argument) - else: - raise TypeError(f"Invalid converter: {converter}") - - if self.choices: - choices_names: list[str | int | float] = [choice.name for choice in self.choices] - if converted in choices_names and (choice := find(lambda c: c.name == converted, self.choices)): - converted = choice.value - else: - choices = [choice.value for choice in self.choices] - if converted not in choices: - raise ValueError( - f"{argument} is not a valid choice. Valid choices: {list(set(choices_names + choices))}" - ) - - return converted - except ValueError as exc: - raise BadArgument() from exc - - -def bridge_option(name, input_type=None, **kwargs): - """A decorator that can be used instead of typehinting :class:`.BridgeOption`. - - .. versionadded:: 2.6 - - Attributes - ---------- - parameter_name: :class:`str` - The name of the target function parameter this option is mapped to. - This allows you to have a separate UI ``name`` and parameter name. - """ - - def decorator(func): - resolved_name = kwargs.pop("parameter_name", None) or name - itype = kwargs.pop("type", None) or input_type or func.__annotations__.get(resolved_name, str) - func.__annotations__[resolved_name] = BridgeOption(itype, name=name, **kwargs) - return func - - return decorator diff --git a/discord/ext/commands/__init__.py b/discord/ext/commands/__init__.py deleted file mode 100644 index b13b484c68..0000000000 --- a/discord/ext/commands/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -discord.ext.commands -~~~~~~~~~~~~~~~~~~~~~ - -An extension module to facilitate creation of bot commands. - -:copyright: (c) 2015-2021 Rapptz & (c) 2021-present Pycord Development -:license: MIT, see LICENSE for more details. -""" - -from .bot import * -from .cog import * -from .context import * -from .converter import * -from .cooldowns import * -from .core import * -from .errors import * -from .flags import * -from .help import * diff --git a/discord/ext/commands/_types.py b/discord/ext/commands/_types.py deleted file mode 100644 index 4a7140cef5..0000000000 --- a/discord/ext/commands/_types.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from typing import TYPE_CHECKING, Any, Callable, Coroutine, TypeVar, Union - -if TYPE_CHECKING: - from .cog import Cog - from .context import Context - from .errors import CommandError - -T = TypeVar("T") - -Coro = Coroutine[Any, Any, T] -MaybeCoro = T | Coro[T] -CoroFunc = Callable[..., Coro[Any]] - -Check = Callable[["Cog", "Context[Any]"], MaybeCoro[bool]] | Callable[["Context[Any]"], MaybeCoro[bool]] -Hook = Callable[["Cog", "Context[Any]"], Coro[Any]] | Callable[["Context[Any]"], Coro[Any]] -Error = ( - Callable[["Cog", "Context[Any]", "CommandError"], Coro[Any]] | Callable[["Context[Any]", "CommandError"], Coro[Any]] -) diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py deleted file mode 100644 index 831340ac80..0000000000 --- a/discord/ext/commands/bot.py +++ /dev/null @@ -1,450 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -import collections -import collections.abc -import sys -import traceback -from typing import TYPE_CHECKING, Any, Callable, Coroutine, Iterable, TypeVar - -import discord -from discord.utils import Undefined -from discord.utils.private import async_all, copy_doc, maybe_awaitable - -from . import errors -from .context import Context -from .core import GroupMixin -from .help import DefaultHelpCommand, HelpCommand -from .view import StringView - -if TYPE_CHECKING: - from discord.message import Message - - from ._types import CoroFunc - -__all__ = ( - "when_mentioned", - "when_mentioned_or", - "Bot", - "AutoShardedBot", -) - -MISSING: Any = discord.utils.MISSING - -T = TypeVar("T") -CFT = TypeVar("CFT", bound="CoroFunc") -CXT = TypeVar("CXT", bound="Context") - - -def when_mentioned(bot: Bot | AutoShardedBot, msg: Message) -> list[str]: - """A callable that implements a command prefix equivalent to being mentioned. - - These are meant to be passed into the :attr:`.Bot.command_prefix` attribute. - """ - # bot.user will never be None when this is called - return [f"<@{bot.user.id}> ", f"<@!{bot.user.id}> "] # type: ignore - - -def when_mentioned_or( - *prefixes: str, -) -> Callable[[Bot | AutoShardedBot, Message], list[str]]: - """A callable that implements when mentioned or other prefixes provided. - - These are meant to be passed into the :attr:`.Bot.command_prefix` attribute. - - See Also - -------- - :func:`.when_mentioned` - - Example - ------- - - .. code-block:: python3 - - bot = commands.Bot(command_prefix=commands.when_mentioned_or("!")) - - .. note:: - - This callable returns another callable, so if this is done inside a custom - callable, you must call the returned callable, for example: - - .. code-block:: python3 - - async def get_prefix(bot, message): - extras = await prefixes_for(message.guild) # returns a list - return commands.when_mentioned_or(*extras)(bot, message) - """ - - def inner(bot, msg): - r = list(prefixes) - r = when_mentioned(bot, msg) + r - return r - - return inner - - -def _is_submodule(parent: str, child: str) -> bool: - return parent == child or child.startswith(f"{parent}.") - - -class BotBase(GroupMixin, discord.cog.CogMixin): - _help_command = None - _supports_prefixed_commands = True - - def __init__( - self, - command_prefix: ( - str - | Iterable[str] - | Callable[ - [Bot | AutoShardedBot, Message], - str | Iterable[str] | Coroutine[Any, Any, str | Iterable[str]], - ] - ) = when_mentioned, - help_command: HelpCommand | None | Undefined = MISSING, - **options, - ): - super().__init__(**options) - self.command_prefix = command_prefix - self.help_command = DefaultHelpCommand() if help_command is MISSING else help_command - self.strip_after_prefix = options.get("strip_after_prefix", False) - - @copy_doc(discord.Client.close) - async def close(self) -> None: - for extension in tuple(self.__extensions): - try: - self.unload_extension(extension) - except Exception: - pass - - for cog in tuple(self.__cogs): - try: - self.remove_cog(cog) - except Exception: - pass - - await super().close() # type: ignore - - async def on_command_error(self, context: Context, exception: errors.CommandError) -> None: - """|coro| - - The default command error handler provided by the bot. - - By default, this prints to :data:`sys.stderr` however it could be - overridden to have a different implementation. - - This only fires if you do not specify any listeners for command error. - """ - if self._event_handlers.get("on_command_error", None): - return - - command = context.command - if command and command.has_error_handler(): - return - - cog = context.cog - if cog and cog.has_error_handler(): - return - - print(f"Ignoring exception in command {context.command}:", file=sys.stderr) - traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr) - - async def can_run(self, ctx: Context, *, call_once: bool = False) -> bool: - data = self._check_once if call_once else self._checks - - if len(data) == 0: - return True - - # type-checker doesn't distinguish between functions and methods - return await async_all(f(ctx) for f in data) # type: ignore - - # help command stuff - - @property - def help_command(self) -> HelpCommand | None: - return self._help_command - - @help_command.setter - def help_command(self, value: HelpCommand | None) -> None: - if value is not None: - if not isinstance(value, HelpCommand): - raise TypeError("help_command must be a subclass of HelpCommand") - if self._help_command is not None: - self._help_command._remove_from_bot(self) - self._help_command = value - value._add_to_bot(self) - elif self._help_command is not None: - self._help_command._remove_from_bot(self) - self._help_command = None - else: - self._help_command = None - - # command processing - - async def get_prefix(self, message: Message) -> list[str] | str: - """|coro| - - Retrieves the prefix the bot is listening to - with the message as a context. - - Parameters - ---------- - message: :class:`discord.Message` - The message context to get the prefix of. - - Returns - ------- - Union[List[:class:`str`], :class:`str`] - A list of prefixes or a single prefix that the bot is - listening for. - """ - prefix = ret = self.command_prefix - if callable(prefix): - ret = await maybe_awaitable(prefix, self, message) - - if not isinstance(ret, str): - try: - ret = list(ret) - except TypeError as e: - # It's possible that a generator raised this exception. Don't - # replace it with our own error if that's the case. - if isinstance(ret, collections.abc.Iterable): - raise - - raise TypeError( - "command_prefix must be plain string, iterable of strings, or" - f" callable returning either of these, not {ret.__class__.__name__}" - ) from e - - if not ret: - raise ValueError("Iterable command_prefix must contain at least one prefix") - - return ret - - async def get_context(self, message: Message, *, cls: type[CXT] = Context) -> CXT: - r"""|coro| - - Returns the invocation context from the message. - - This is a more low-level counter-part for :meth:`.process_commands` - to allow users more fine-grained control over the processing. - - The returned context is not guaranteed to be a valid invocation - context, :attr:`.Context.valid` must be checked to make sure it is. - If the context is not valid then it is not a valid candidate to be - invoked under :meth:`~.Bot.invoke`. - - Parameters - ----------- - message: :class:`discord.Message` - The message to get the invocation context from. - cls - The factory class that will be used to create the context. - By default, this is :class:`.Context`. Should a custom - class be provided, it must be similar enough to :class:`.Context`\'s - interface. - - Returns - -------- - :class:`.Context` - The invocation context. The type of this can change via the - ``cls`` parameter. - """ - - view = StringView(message.content) - ctx = cls(prefix=None, view=view, bot=self, message=message) - - if message.author.id == self.user.id: # type: ignore - return ctx - - prefix = await self.get_prefix(message) - invoked_prefix = prefix - - if isinstance(prefix, str): - if not view.skip_string(prefix): - return ctx - else: - try: - # if the context class' __init__ consumes something from the view this - # will be wrong. That seems unreasonable though. - if message.content.startswith(tuple(prefix)): - invoked_prefix = discord.utils.find(view.skip_string, prefix) - else: - return ctx - - except TypeError as e: - if not isinstance(prefix, list): - raise TypeError( - f"get_prefix must return either a string or a list of string, not {prefix.__class__.__name__}" - ) from e - - # It's possible a bad command_prefix got us here. - for value in prefix: - if not isinstance(value, str): - raise TypeError( - "Iterable command_prefix or list returned from get_prefix" - " must contain only strings, not" - f" {value.__class__.__name__}" - ) from e - - # Getting here shouldn't happen - raise - - if self.strip_after_prefix: - view.skip_ws() - - invoker = view.get_word() - ctx.invoked_with = invoker - # type-checker fails to narrow invoked_prefix type. - ctx.prefix = invoked_prefix # type: ignore - ctx.command = self.prefixed_commands.get(invoker) - return ctx - - async def invoke(self, ctx: Context) -> None: - """|coro| - - Invokes the command given under the invocation context and - handles all the internal event dispatch mechanisms. - - Parameters - ---------- - ctx: :class:`.Context` - The invocation context to invoke. - """ - if ctx.command is not None: - self.dispatch("command", ctx) - try: - if await self.can_run(ctx, call_once=True): - await ctx.command.invoke(ctx) - else: - raise errors.CheckFailure("The global check once functions failed.") - except errors.CommandError as exc: - await ctx.command.dispatch_error(ctx, exc) - else: - self.dispatch("command_completion", ctx) - elif ctx.invoked_with: - exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found') - self.dispatch("command_error", ctx, exc) - - async def process_commands(self, message: Message) -> None: - """|coro| - - This function processes the commands that have been registered - to the bot and other groups. Without this coroutine, none of the - commands will be triggered. - - By default, this coroutine is called inside the :func:`.on_message` - event. If you choose to override the :func:`.on_message` event, then - you should invoke this coroutine as well. - - This is built using other low level tools, and is equivalent to a - call to :meth:`~.Bot.get_context` followed by a call to :meth:`~.Bot.invoke`. - - This also checks if the message's author is a bot and doesn't - call :meth:`~.Bot.get_context` or :meth:`~.Bot.invoke` if so. - - Parameters - ---------- - message: :class:`discord.Message` - The message to process commands for. - """ - if message.author.bot: - return - - ctx = await self.get_context(message) - await self.invoke(ctx) - - async def on_message(self, message): - await self.process_commands(message) - - -class Bot(BotBase, discord.Bot): - """Represents a discord bot. - - This class is a subclass of :class:`discord.Bot` and as a result - anything that you can do with a :class:`discord.Bot` you can do with - this bot. - - This class also subclasses :class:`.GroupMixin` to provide the functionality - to manage commands. - - .. note:: - - Using prefixed commands requires :attr:`discord.Intents.message_content` to be enabled. - - Attributes - ---------- - command_prefix - The command prefix is what the message content must contain initially - to have a command invoked. This prefix could either be a string to - indicate what the prefix should be, or a callable that takes in the bot - as its first parameter and :class:`discord.Message` as its second - parameter and returns the prefix. This is to facilitate "dynamic" - command prefixes. This callable can be either a regular function or - a coroutine. - - An empty string as the prefix always matches, enabling prefix-less - command invocation. While this may be useful in DMs it should be avoided - in servers, as it's likely to cause performance issues and unintended - command invocations. - - The command prefix could also be an iterable of strings indicating that - multiple checks for the prefix should be used and the first one to - match will be the invocation prefix. You can get this prefix via - :attr:`.Context.prefix`. To avoid confusion empty iterables are not - allowed. - - .. note:: - - When passing multiple prefixes be careful to not pass a prefix - that matches a longer prefix occurring later in the sequence. For - example, if the command prefix is ``('!', '!?')`` the ``'!?'`` - prefix will never be matched to any message as the previous one - matches messages starting with ``!?``. This is especially important - when passing an empty string, it should always be last as no prefix - after it will be matched. - case_insensitive: :class:`bool` - Whether the commands should be case-insensitive. Defaults to ``False``. This - attribute does not carry over to groups. You must set it to every group if - you require group commands to be case-insensitive as well. - help_command: Optional[:class:`.HelpCommand`] - The help command implementation to use. This can be dynamically - set at runtime. To remove the help command pass ``None``. For more - information on implementing a help command, see :ref:`ext_commands_help_command`. - strip_after_prefix: :class:`bool` - Whether to strip whitespace characters after encountering the command - prefix. This allows for ``! hello`` and ``!hello`` to both work if - the ``command_prefix`` is set to ``!``. Defaults to ``False``. - - .. versionadded:: 1.7 - """ - - -class AutoShardedBot(BotBase, discord.AutoShardedBot): - """This is similar to :class:`.Bot` except that it is inherited from - :class:`discord.AutoShardedBot` instead. - """ diff --git a/discord/ext/commands/cog.py b/discord/ext/commands/cog.py deleted file mode 100644 index 871d3e816e..0000000000 --- a/discord/ext/commands/cog.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Callable, Generator, TypeVar - -import discord - -from ...cog import Cog -from ...commands import ApplicationCommand, SlashCommandGroup - -if TYPE_CHECKING: - from .core import Command - -__all__ = ("Cog",) - -CogT = TypeVar("CogT", bound="Cog") -FuncT = TypeVar("FuncT", bound=Callable[..., Any]) - -MISSING: Any = discord.utils.MISSING - - -class Cog(Cog): - def __new__(cls: type[CogT], *args: Any, **kwargs: Any) -> CogT: - # For issue 426, we need to store a copy of the command objects - # since we modify them to inject `self` to them. - # To do this, we need to interfere with the Cog creation process. - return super().__new__(cls) - - def walk_commands(self) -> Generator[Command]: - """An iterator that recursively walks through this cog's commands and subcommands. - - Yields - ------ - Union[:class:`.Command`, :class:`.Group`] - A command or group from the cog. - """ - from .core import GroupMixin - - for command in self.__cog_commands__: - if not isinstance(command, ApplicationCommand): - if command.parent is None: - yield command - if isinstance(command, GroupMixin): - yield from command.walk_commands() - elif isinstance(command, SlashCommandGroup): - yield from command.walk_commands() - else: - yield command - - def get_commands(self) -> list[ApplicationCommand | Command]: - r""" - Returns - -------- - List[Union[:class:`~discord.ApplicationCommand`, :class:`.Command`]] - A :class:`list` of commands that are defined inside this cog. - - .. note:: - - This does not include subcommands. - """ - return [c for c in self.__cog_commands__ if c.parent is None] diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py deleted file mode 100644 index 252d451cd2..0000000000 --- a/discord/ext/commands/context.py +++ /dev/null @@ -1,405 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -import inspect -import re -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union - -import discord.abc -from discord.message import Message -from discord.utils import MISSING, Undefined -from discord.utils.private import copy_doc - -if TYPE_CHECKING: - from typing_extensions import ParamSpec - - from discord.abc import MessageableChannel - from discord.app.state import ConnectionState - from discord.guild import Guild - from discord.member import Member - from discord.user import ClientUser, User - from discord.voice_client import VoiceProtocol - - from .bot import AutoShardedBot, Bot - from .cog import Cog - from .core import Command - from .view import StringView - -__all__ = ("Context",) - - -T = TypeVar("T") -BotT = TypeVar("BotT", bound="Bot | AutoShardedBot") -CogT = TypeVar("CogT", bound="Cog") - -if TYPE_CHECKING: - P = ParamSpec("P") -else: - P = TypeVar("P") - - -class Context(discord.abc.Messageable, Generic[BotT]): - r"""Represents the context in which a command is being invoked under. - - This class contains a lot of metadata to help you understand more about - the invocation context. This class is not created manually and is instead - passed around to commands as the first parameter. - - This class implements the :class:`~discord.abc.Messageable` ABC. - - Attributes - ----------- - message: :class:`.Message` - The message that triggered the command being executed. - bot: :class:`.Bot` - The bot that contains the command being executed. - args: :class:`list` - The list of transformed arguments that were passed into the command. - If this is accessed during the :func:`.on_command_error` event - then this list could be incomplete. - kwargs: :class:`dict` - A dictionary of transformed arguments that were passed into the command. - Similar to :attr:`args`\, if this is accessed in the - :func:`.on_command_error` event then this dict could be incomplete. - current_parameter: Optional[:class:`inspect.Parameter`] - The parameter that is currently being inspected and converted. - This is only of use for within converters. - - .. versionadded:: 2.0 - prefix: Optional[:class:`str`] - The prefix that was used to invoke the command. - command: Optional[:class:`Command`] - The command that is being invoked currently. - invoked_with: Optional[:class:`str`] - The command name that triggered this invocation. Useful for finding out - which alias called the command. - invoked_parents: List[:class:`str`] - The command names of the parents that triggered this invocation. Useful for - finding out which aliases called the command. - - For example in commands ``?a b c test``, the invoked parents are ``['a', 'b', 'c']``. - - .. versionadded:: 1.7 - - invoked_subcommand: Optional[:class:`Command`] - The subcommand that was invoked. - If no valid subcommand was invoked then this is equal to ``None``. - subcommand_passed: Optional[:class:`str`] - The string that was attempted to call a subcommand. This does not have - to point to a valid registered subcommand and could just point to a - nonsense string. If nothing was passed to attempt a call to a - subcommand then this is set to ``None``. - command_failed: :class:`bool` - A boolean that indicates if the command failed to be parsed, checked, - or invoked. - """ - - def __init__( - self, - *, - message: Message, - bot: BotT, - view: StringView, - args: list[Any] | Undefined = MISSING, - kwargs: dict[str, Any] | Undefined = MISSING, - prefix: str | None = None, - command: Command | None = None, - invoked_with: str | None = None, - invoked_parents: list[str] | Undefined = MISSING, - invoked_subcommand: Command | None = None, - subcommand_passed: str | None = None, - command_failed: bool = False, - current_parameter: inspect.Parameter | None = None, - ): - self.message: Message = message - self.bot: BotT = bot - self.args: list[Any] = args or [] - self.kwargs: dict[str, Any] = kwargs or {} - self.prefix: str | None = prefix - self.command: Command | None = command - self.view: StringView = view - self.invoked_with: str | None = invoked_with - self.invoked_parents: list[str] = invoked_parents or [] - self.invoked_subcommand: Command | None = invoked_subcommand - self.subcommand_passed: str | None = subcommand_passed - self.command_failed: bool = command_failed - self.current_parameter: inspect.Parameter | None = current_parameter - self._state: ConnectionState = self.message._state - - async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T: - r"""|coro| - - Calls a command with the arguments given. - - This is useful if you want to just call the callback that a - :class:`.Command` holds internally. - - .. note:: - - This does not handle converters, checks, cooldowns, pre-invoke, - or after-invoke hooks in any matter. It calls the internal callback - directly as-if it was a regular function. - - You must take care in passing the proper arguments when - using this function. - - Parameters - ----------- - command: :class:`.Command` - The command that is going to be called. - \*args - The arguments to use. - \*\*kwargs - The keyword arguments to use. - - Raises - ------- - TypeError - The command argument to invoke is missing. - """ - return await command(self, *args, **kwargs) - - async def reinvoke(self, *, call_hooks: bool = False, restart: bool = True) -> None: - """|coro| - - Calls the command again. - - This is similar to :meth:`~.Context.invoke` except that it bypasses - checks, cooldowns, and error handlers. - - .. note:: - - If you want to bypass :exc:`.UserInputError` derived exceptions, - it is recommended to use the regular :meth:`~.Context.invoke` - as it will work more naturally. After all, this will end up - using the old arguments the user has used and will thus just - fail again. - - Parameters - ---------- - call_hooks: :class:`bool` - Whether to call the before and after invoke hooks. - restart: :class:`bool` - Whether to start the call chain from the very beginning - or where we left off (i.e. the command that caused the error). - The default is to start where we left off. - - Raises - ------ - ValueError - The context to reinvoke is not valid. - """ - cmd = self.command - view = self.view - if cmd is None: - raise ValueError("This context is not valid.") - - # some state to revert to when we're done - index, previous = view.index, view.previous - invoked_with = self.invoked_with - invoked_subcommand = self.invoked_subcommand - invoked_parents = self.invoked_parents - subcommand_passed = self.subcommand_passed - - if restart: - to_call = cmd.root_parent or cmd - view.index = len(self.prefix or "") - view.previous = 0 - self.invoked_parents = [] - self.invoked_with = view.get_word() # advance to get the root command - else: - to_call = cmd - - try: - await to_call.reinvoke(self, call_hooks=call_hooks) - finally: - self.command = cmd - view.index = index - view.previous = previous - self.invoked_with = invoked_with - self.invoked_subcommand = invoked_subcommand - self.invoked_parents = invoked_parents - self.subcommand_passed = subcommand_passed - - @property - def valid(self) -> bool: - """Checks if the invocation context is valid to be invoked with.""" - return self.prefix is not None and self.command is not None - - async def _get_channel(self) -> discord.abc.Messageable: - return self.channel - - @property - def clean_prefix(self) -> str: - """The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``. - - .. versionadded:: 2.0 - """ - if self.prefix is None: - return "" - - user = self.me - # this breaks if the prefix mention is not the bot itself, but I - # consider this to be an *incredibly* strange use case. I'd rather go - # for this common use case rather than waste performance for the - # odd one. - pattern = re.compile(r"<@!?%s>" % user.id) - return pattern.sub("@%s" % user.display_name.replace("\\", r"\\"), self.prefix) - - @property - def cog(self) -> Cog | None: - """Returns the cog associated with this context's command. - None if it does not exist. - """ - - if self.command is None: - return None - return self.command.cog - - @property - def guild(self) -> Guild | None: - """Returns the guild associated with this context's command. - None if not available. - """ - return self.message.guild - - @property - def channel(self) -> MessageableChannel: - """Returns the channel associated with this context's command. - Shorthand for :attr:`.Message.channel`. - """ - return self.message.channel - - @property - def author(self) -> User | Member: - """Union[:class:`~discord.User`, :class:`.Member`]: - Returns the author associated with this context's command. Shorthand for :attr:`.Message.author` - """ - return self.message.author - - @property - def me(self) -> Member | ClientUser: - """Union[:class:`.Member`, :class:`.ClientUser`]: - Similar to :attr:`.Guild.me` except it may return the :class:`.ClientUser` in private message - message contexts, or when :meth:`Intents.guilds` is absent. - """ - # bot.user will never be None at this point. - return self.guild.me if self.guild is not None and self.guild.me is not None else self.bot.user # type: ignore - - @property - def voice_client(self) -> VoiceProtocol | None: - r"""A shortcut to :attr:`.Guild.voice_client`\, if applicable.""" - g = self.guild - return g.voice_client if g else None - - async def send_help(self, *args: Any) -> Any: - """send_help(entity=) - - |coro| - - Shows the help command for the specified entity if given. - The entity can be a command or a cog. - - If no entity is given, then it'll show help for the - entire bot. - - If the entity is a string, then it looks up whether it's a - :class:`Cog` or a :class:`Command`. - - .. note:: - - Due to the way this function works, instead of returning - something similar to :meth:`~.commands.HelpCommand.command_not_found` - this returns :class:`None` on bad input or no help command. - - Parameters - ---------- - entity: Optional[Union[:class:`Command`, :class:`Cog`, :class:`str`]] - The entity to show help for. - - Returns - ------- - Any - The result of the help command, if any. - """ - from .core import Command, Group, wrap_callback - from .errors import CommandError - - bot = self.bot - cmd = bot.help_command - - if cmd is None: - return None - - cmd = cmd.copy() - cmd.context = self - if len(args) == 0: - await cmd.prepare_help_command(self, None) - mapping = cmd.get_bot_mapping() - injected = wrap_callback(cmd.send_bot_help) - try: - return await injected(mapping) - except CommandError as e: - await cmd.on_help_command_error(self, e) - return None - - entity = args[0] - if isinstance(entity, str): - entity = bot.get_cog(entity) or bot.get_command(entity) - - if entity is None: - return None - - if not hasattr(entity, "qualified_name"): - # TODO: this is ugly shit please make it better like isinstance idk - # if we're here then it's not a cog, group, or command. - return None - - await cmd.prepare_help_command(self, entity.qualified_name) - - try: - if hasattr(entity, "__cog_commands__"): - injected = wrap_callback(cmd.send_cog_help) - return await injected(entity) - elif isinstance(entity, Group): - injected = wrap_callback(cmd.send_group_help) - return await injected(entity) - elif isinstance(entity, Command): - injected = wrap_callback(cmd.send_command_help) - return await injected(entity) - else: - return None - except CommandError as e: - await cmd.on_help_command_error(self, e) - - @copy_doc(Message.reply) - async def reply(self, content: str | None = None, **kwargs: Any) -> Message: - return await self.message.reply(content, **kwargs) - - @copy_doc(Message.forward_to) - async def forward_to(self, channel: discord.abc.Messageable, **kwargs: Any) -> Message: - return await self.message.forward_to(channel, **kwargs) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py deleted file mode 100644 index fb838d43bf..0000000000 --- a/discord/ext/commands/converter.py +++ /dev/null @@ -1,1209 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -import inspect -import re -from typing import ( - TYPE_CHECKING, - Any, - Generic, - Iterable, - List, - Literal, - Protocol, - TypeVar, - Union, - runtime_checkable, -) - -import discord -from discord.utils import UNICODE_EMOJIS - -from .errors import * - -if TYPE_CHECKING: - from discord.message import PartialMessageableChannel - - from .context import Context - - -__all__ = ( - "Converter", - "ObjectConverter", - "MemberConverter", - "UserConverter", - "MessageConverter", - "PartialMessageConverter", - "TextChannelConverter", - "ForumChannelConverter", - "InviteConverter", - "GuildConverter", - "RoleConverter", - "GameConverter", - "ColourConverter", - "ColorConverter", - "VoiceChannelConverter", - "StageChannelConverter", - "EmojiConverter", - "PartialEmojiConverter", - "CategoryChannelConverter", - "IDConverter", - "ThreadConverter", - "GuildChannelConverter", - "GuildStickerConverter", - "clean_content", - "Greedy", - "run_converters", -) - - -async def _get_from_guilds(bot, getter, argument): - result = None - for guild in await bot.get_guilds(): - result = getattr(guild, getter)(argument) - if result: - return result - return result - - -T = TypeVar("T") -T_co = TypeVar("T_co", covariant=True) -CT = TypeVar("CT", bound=discord.channel.GuildChannel) -TT = TypeVar("TT", bound=discord.Thread) - - -@runtime_checkable -class Converter(Protocol[T_co]): - """The base class of custom converters that require the :class:`.Context` - to be passed to be useful. - - This allows you to implement converters that function similar to the - special cased ``discord`` classes. - - Classes that derive from this should override the :meth:`~.Converter.convert` - method to do its conversion logic. This method must be a :ref:`coroutine `. - """ - - async def convert(self, ctx: Context, argument: str) -> T_co: - """|coro| - - The method to override to do conversion logic. - - If an error is found while converting, it is recommended to - raise a :exc:`.CommandError` derived exception as it will - properly propagate to the error handlers. - - Parameters - ---------- - ctx: :class:`.Context` - The invocation context that the argument is being used in. - argument: :class:`str` - The argument that is being converted. - - Raises - ------ - :exc:`.CommandError` - A generic exception occurred when converting the argument. - :exc:`.BadArgument` - The converter failed to convert the argument. - """ - raise NotImplementedError("Derived classes need to implement this.") - - -_ID_REGEX = re.compile(r"([0-9]{15,20})$") - - -class IDConverter(Converter[T_co]): - @staticmethod - def _get_id_match(argument): - return _ID_REGEX.match(argument) - - -class ObjectConverter(IDConverter[discord.Object]): - """Converts to a :class:`~discord.Object`. - - The argument must follow the valid ID or mention formats (e.g. `<@80088516616269824>`). - - .. versionadded:: 2.0 - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by member, role, or channel mention. - """ - - async def convert(self, ctx: Context, argument: str) -> discord.Object: - match = self._get_id_match(argument) or re.match(r"<(?:@[!&]?|#)([0-9]{15,20})>$", argument) - - if match is None: - raise ObjectNotFound(argument) - - result = int(match.group(1)) - - return discord.Object(id=result) - - -class MemberConverter(IDConverter[discord.Member]): - """Converts to a :class:`~discord.Member`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name#discrim - 4. Lookup by name - 5. Lookup by nickname - - .. versionchanged:: 1.5 - Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument` - - .. versionchanged:: 1.5.1 - This converter now lazily fetches members from the gateway and HTTP APIs, - optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled. - """ - - async def query_member_named(self, guild, argument): - cache = guild._state.member_cache_flags.joined - if len(argument) > 5 and argument[-5] == "#": - username, _, discriminator = argument.rpartition("#") - members = await guild.query_members(username, limit=100, cache=cache) - return discord.utils.find(lambda m: m.name == username and m.discriminator == discriminator, members) - members = await guild.query_members(argument, limit=100, cache=cache) - return discord.utils.find( - lambda m: argument in (m.nick, m.name, m.global_name), - members, - ) - - async def query_member_by_id(self, bot, guild, user_id): - ws = bot._get_websocket(shard_id=guild.shard_id) - cache = guild._state.member_cache_flags.joined - if ws.is_ratelimited(): - # If we're being rate limited on the WS, then fall back to using the HTTP API - # So we don't have to wait ~60 seconds for the query to finish - try: - member = await guild.fetch_member(user_id) - except discord.HTTPException: - return None - - if cache: - await guild._add_member(member) - return member - - # If we're not being rate limited then we can use the websocket to actually query - members = await guild.query_members(limit=1, user_ids=[user_id], cache=cache) - if not members: - return None - return members[0] - - async def convert(self, ctx: Context, argument: str) -> discord.Member: - bot = ctx.bot - match = self._get_id_match(argument) or re.match(r"<@!?([0-9]{15,20})>$", argument) - guild = ctx.guild - result = None - user_id = None - if match is None: - # not a mention... - if guild: - result = guild.get_member_named(argument) - else: - result = await _get_from_guilds(bot, "get_member_named", argument) - else: - user_id = int(match.group(1)) - if guild: - result = await guild.get_member(user_id) - if ctx.message is not None and result is None: - result = discord.utils.find(lambda e: e.id == user_id, ctx.message.mentions) - else: - result = await _get_from_guilds(bot, "get_member", user_id) - - if result is None: - if guild is None: - raise MemberNotFound(argument) - - if user_id is not None: - result = await self.query_member_by_id(bot, guild, user_id) - else: - result = await self.query_member_named(guild, argument) - - if not result: - raise MemberNotFound(argument) - - return result - - -class UserConverter(IDConverter[discord.User]): - """Converts to a :class:`~discord.User`. - - All lookups are via the global user cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name#discrim - 4. Lookup by name - - .. versionchanged:: 1.5 - Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument` - - .. versionchanged:: 1.6 - This converter now lazily fetches users from the HTTP APIs if an ID is - passed, and it's not available in cache. - """ - - async def convert(self, ctx: Context, argument: str) -> discord.User: - match = self._get_id_match(argument) or re.match(r"<@!?([0-9]{15,20})>$", argument) - result = None - state = ctx._state - - if match is not None: - user_id = int(match.group(1)) - result = await ctx.bot.get_user(user_id) - if ctx.message is not None and result is None: - result = discord.utils.find(lambda e: e.id == user_id, ctx.message.mentions) - if result is None: - try: - result = await ctx.bot.fetch_user(user_id) - except discord.HTTPException: - raise UserNotFound(argument) from None - - return result - - arg = argument - - # Remove the '@' character if this is the first character from the argument - if arg[0] == "@": - # Remove first character - arg = arg[1:] - - # check for discriminator if it exists, - if len(arg) > 5 and arg[-5] == "#": - discrim = arg[-4:] - name = arg[:-5] - predicate = lambda u: u.name == name and u.discriminator == discrim - result = discord.utils.find(predicate, await state.cache.get_all_users()) - if result is not None: - return result - - predicate = lambda u: arg in (u.name, u.global_name) - result = discord.utils.find(predicate, await state.cache.get_all_users()) - - if result is None: - raise UserNotFound(argument) - - return result - - -class PartialMessageConverter(Converter[discord.PartialMessage]): - """Converts to a :class:`discord.PartialMessage`. - - .. versionadded:: 1.7 - - The creation strategy is as follows (in order): - - 1. By "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID") - 2. By message ID (The message is assumed to be in the context channel.) - 3. By message URL - """ - - @staticmethod - def _get_id_matches(ctx, argument): - id_regex = re.compile(r"(?:(?P[0-9]{15,20})-)?(?P[0-9]{15,20})$") - link_regex = re.compile( - r"https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/" - r"(?P[0-9]{15,20}|@me)" - r"/(?P[0-9]{15,20})/(?P[0-9]{15,20})/?$" - ) - match = id_regex.match(argument) or link_regex.match(argument) - if not match: - raise MessageNotFound(argument) - data = match.groupdict() - channel_id = data.get("channel_id") - if channel_id is None: - channel_id = ctx.channel and ctx.channel.id - else: - channel_id = int(channel_id) - message_id = int(data["message_id"]) - guild_id = data.get("guild_id") - if guild_id is None: - guild_id = ctx.guild and ctx.guild.id - elif guild_id == "@me": - guild_id = None - else: - guild_id = int(guild_id) - return guild_id, message_id, channel_id - - @staticmethod - def _resolve_channel(ctx, guild_id, channel_id) -> PartialMessageableChannel | None: - if guild_id is not None: - guild = ctx.bot.get_guild(guild_id) - if guild is not None and channel_id is not None: - return guild._resolve_channel(channel_id) # type: ignore - else: - return None - else: - return ctx.bot.get_channel(channel_id) if channel_id else ctx.channel - - async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage: - guild_id, message_id, channel_id = self._get_id_matches(ctx, argument) - channel = self._resolve_channel(ctx, guild_id, channel_id) - if not channel: - raise ChannelNotFound(channel_id) - return discord.PartialMessage(channel=channel, id=message_id) - - -class MessageConverter(IDConverter[discord.Message]): - """Converts to a :class:`discord.Message`. - - .. versionadded:: 1.1 - - The lookup strategy is as follows (in order): - - 1. Lookup by "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID") - 2. Lookup by message ID (the message **must** be in the context channel) - 3. Lookup by message URL - - .. versionchanged:: 1.5 - Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` - instead of generic :exc:`.BadArgument` - """ - - async def convert(self, ctx: Context, argument: str) -> discord.Message: - guild_id, message_id, channel_id = PartialMessageConverter._get_id_matches(ctx, argument) - message = await ctx.bot._connection._get_message(message_id) - if message: - return message - channel = PartialMessageConverter._resolve_channel(ctx, guild_id, channel_id) - if not channel: - raise ChannelNotFound(channel_id) - try: - return await channel.fetch_message(message_id) - except discord.NotFound as e: - raise MessageNotFound(argument) from e - except discord.Forbidden as e: - raise ChannelNotReadable(channel) from e - - -class GuildChannelConverter(IDConverter[discord.channel.GuildChannel]): - """Converts to a :class:`~discord.abc.GuildChannel`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name. - - .. versionadded:: 2.0 - """ - - async def convert(self, ctx: Context, argument: str) -> discord.channel.GuildChannel: - return await self._resolve_channel(ctx, argument, "channels", discord.channel.base.GuildChannel) - - @staticmethod - async def _resolve_channel(ctx: Context, argument: str, attribute: str, type: type[CT]) -> CT: - bot = ctx.bot - - match = IDConverter._get_id_match(argument) or re.match(r"<#([0-9]{15,20})>$", argument) - result = None - guild = ctx.guild - - if match is None: - # not a mention - if guild: - iterable: Iterable[CT] = getattr(guild, attribute) - result: CT | None = discord.utils.find(lambda e: e.name == argument, iterable) - else: - - def check(c): - return isinstance(c, type) and c.name == argument - - result = discord.utils.find(check, bot.get_all_channels()) - else: - channel_id = int(match.group(1)) - if guild: - result = guild.get_channel(channel_id) - else: - result = await _get_from_guilds(bot, "get_channel", channel_id) - - if not isinstance(result, type): - raise ChannelNotFound(argument) - - return result - - @staticmethod - def _resolve_thread(ctx: Context, argument: str, attribute: str, type: type[TT]) -> TT: - match = IDConverter._get_id_match(argument) or re.match(r"<#([0-9]{15,20})>$", argument) - result = None - guild = ctx.guild - - if match is None: - # not a mention - if guild: - iterable: Iterable[TT] = getattr(guild, attribute) - result: TT | None = discord.utils.find(lambda e: e.name == argument, iterable) - else: - thread_id = int(match.group(1)) - if guild: - result = guild.get_thread(thread_id) - - if not result or not isinstance(result, type): - raise ThreadNotFound(argument) - - return result - - -class TextChannelConverter(IDConverter[discord.TextChannel]): - """Converts to a :class:`~discord.TextChannel`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - - .. versionchanged:: 1.5 - Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` - """ - - async def convert(self, ctx: Context, argument: str) -> discord.TextChannel: - return GuildChannelConverter._resolve_channel(ctx, argument, "text_channels", discord.TextChannel) - - -class VoiceChannelConverter(IDConverter[discord.VoiceChannel]): - """Converts to a :class:`~discord.VoiceChannel`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - - .. versionchanged:: 1.5 - Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` - """ - - async def convert(self, ctx: Context, argument: str) -> discord.VoiceChannel: - return GuildChannelConverter._resolve_channel(ctx, argument, "voice_channels", discord.VoiceChannel) - - -class StageChannelConverter(IDConverter[discord.StageChannel]): - """Converts to a :class:`~discord.StageChannel`. - - .. versionadded:: 1.7 - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - """ - - async def convert(self, ctx: Context, argument: str) -> discord.StageChannel: - return GuildChannelConverter._resolve_channel(ctx, argument, "stage_channels", discord.StageChannel) - - -class CategoryChannelConverter(IDConverter[discord.CategoryChannel]): - """Converts to a :class:`~discord.CategoryChannel`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - - .. versionchanged:: 1.5 - Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` - """ - - async def convert(self, ctx: Context, argument: str) -> discord.CategoryChannel: - return GuildChannelConverter._resolve_channel(ctx, argument, "categories", discord.CategoryChannel) - - -class ForumChannelConverter(IDConverter[discord.ForumChannel]): - """Converts to a :class:`~discord.ForumChannel`. - - All lookups are via the local guild. If in a DM context, then the lookup - is done by the global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - - .. versionadded:: 2.0 - """ - - async def convert(self, ctx: Context, argument: str) -> discord.ForumChannel: - return GuildChannelConverter._resolve_channel(ctx, argument, "forum_channels", discord.ForumChannel) - - -class ThreadConverter(IDConverter[discord.Thread]): - """Coverts to a :class:`~discord.Thread`. - - All lookups are via the local guild. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name. - - .. versionadded: 2.0 - """ - - async def convert(self, ctx: Context, argument: str) -> discord.Thread: - return GuildChannelConverter._resolve_thread(ctx, argument, "threads", discord.Thread) - - -class ColourConverter(Converter[discord.Colour]): - """Converts to a :class:`~discord.Colour`. - - .. versionchanged:: 1.5 - Add an alias named ColorConverter - - The following formats are accepted: - - - ``0x`` - - ``#`` - - ``0x#`` - - ``rgb(, , )`` - - Any of the ``classmethod`` in :class:`~discord.Colour` - - - The ``_`` in the name can be optionally replaced with spaces. - - Like CSS, ```` can be either 0-255 or 0-100% and ```` can be - either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff). - - .. versionchanged:: 1.5 - Raise :exc:`.BadColourArgument` instead of generic :exc:`.BadArgument` - - .. versionchanged:: 1.7 - Added support for ``rgb`` function and 3-digit hex shortcuts - """ - - RGB_REGEX = re.compile(r"rgb\s*\((?P[0-9]{1,3}%?)\s*,\s*(?P[0-9]{1,3}%?)\s*,\s*(?P[0-9]{1,3}%?)\s*\)") - - def parse_hex_number(self, argument): - arg = "".join(i * 2 for i in argument) if len(argument) == 3 else argument - try: - value = int(arg, base=16) - if not (0 <= value <= 0xFFFFFF): - raise BadColourArgument(argument) - except ValueError: - raise BadColourArgument(argument) from None - else: - return discord.Color(value=value) - - def parse_rgb_number(self, argument, number): - if number[-1] == "%": - value = int(number[:-1]) - if not (0 <= value <= 100): - raise BadColourArgument(argument) - return round(255 * (value / 100)) - - value = int(number) - if not (0 <= value <= 255): - raise BadColourArgument(argument) - return value - - def parse_rgb(self, argument, *, regex=RGB_REGEX): - match = regex.match(argument) - if match is None: - raise BadColourArgument(argument) - - red = self.parse_rgb_number(argument, match.group("r")) - green = self.parse_rgb_number(argument, match.group("g")) - blue = self.parse_rgb_number(argument, match.group("b")) - return discord.Color.from_rgb(red, green, blue) - - async def convert(self, ctx: Context, argument: str) -> discord.Colour: - if argument[0] == "#": - return self.parse_hex_number(argument[1:]) - - if argument[0:2] == "0x": - rest = argument[2:] - # Legacy backwards compatible syntax - if rest.startswith("#"): - return self.parse_hex_number(rest[1:]) - return self.parse_hex_number(rest) - - arg = argument.lower() - if arg[0:3] == "rgb": - return self.parse_rgb(arg) - - arg = arg.replace(" ", "_") - method = getattr(discord.Colour, arg, None) - if arg.startswith("from_") or method is None or not inspect.ismethod(method): - raise BadColourArgument(arg) - return method() - - -ColorConverter = ColourConverter - - -class RoleConverter(IDConverter[discord.Role]): - """Converts to a :class:`~discord.Role`. - - All lookups are via the local guild. If in a DM context, the converter raises - :exc:`.NoPrivateMessage` exception. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by mention. - 3. Lookup by name - - .. versionchanged:: 1.5 - Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument` - """ - - async def convert(self, ctx: Context, argument: str) -> discord.Role: - guild = ctx.guild - if not guild: - raise NoPrivateMessage() - - match = self._get_id_match(argument) or re.match(r"<@&([0-9]{15,20})>$", argument) - if match: - result = guild.get_role(int(match.group(1))) - else: - result = discord.utils.find(lambda e: e.name == argument, guild._roles.values()) - - if result is None: - raise RoleNotFound(argument) - return result - - -class GameConverter(Converter[discord.Game]): - """Converts to :class:`~discord.Game`.""" - - async def convert(self, ctx: Context, argument: str) -> discord.Game: - return discord.Game(name=argument) - - -class InviteConverter(Converter[discord.Invite]): - """Converts to a :class:`~discord.Invite`. - - This is done via an HTTP request using :meth:`.Bot.fetch_invite`. - - .. versionchanged:: 1.5 - Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument` - """ - - async def convert(self, ctx: Context, argument: str) -> discord.Invite: - try: - invite = await ctx.bot.fetch_invite(argument) - return invite - except Exception as exc: - raise BadInviteArgument(argument) from exc - - -class GuildConverter(IDConverter[discord.Guild]): - """Converts to a :class:`~discord.Guild`. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by name. (There is no disambiguation for Guilds with multiple matching names). - - .. versionadded:: 1.7 - """ - - async def convert(self, ctx: Context, argument: str) -> discord.Guild: - match = self._get_id_match(argument) - result = None - - if match is not None: - guild_id = int(match.group(1)) - result = ctx.bot.get_guild(guild_id) - - if result is None: - result = discord.utils.find(lambda e: e.name == argument, await ctx.bot.get_guilds()) - - if result is None: - raise GuildNotFound(argument) - return result - - -class EmojiConverter(IDConverter[discord.GuildEmoji]): - """Converts to a :class:`~discord.GuildEmoji`. - - All lookups are done for the local guild first, if available. If that lookup - fails, then it checks the client's global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 2. Lookup by extracting ID from the emoji. - 3. Lookup by name - - .. versionchanged:: 1.5 - Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument` - """ - - async def convert(self, ctx: Context, argument: str) -> discord.GuildEmoji: - match = self._get_id_match(argument) or re.match(r"$", argument) - result = None - bot = ctx.bot - guild = ctx.guild - - if match is None: - # Try to get the emoji by name. Try local guild first. - if guild: - result = discord.utils.find(lambda e: e.name == argument, guild.emojis) - - if result is None: - result = discord.utils.find(lambda e: e.name == argument, await bot.get_emojis()) - else: - emoji_id = int(match.group(1)) - - # Try to look up emoji by id. - result = await bot.get_emoji(emoji_id) - - if result is None: - raise EmojiNotFound(argument) - - return result - - -class PartialEmojiConverter(Converter[discord.PartialEmoji]): - """Converts to a :class:`~discord.PartialEmoji`. - - This is done by extracting the animated flag, name, and ID for custom emojis, - or by using the standard Unicode emojis supported by Discord. - - .. versionchanged:: 1.5 - Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument` - """ - - async def convert(self, ctx: Context, argument: str) -> discord.PartialEmoji: - match = re.match(r"<(a?):(\w{1,32}):([0-9]{15,20})>$", argument) - - if match: - emoji_animated = bool(match.group(1)) - emoji_name = match.group(2) - emoji_id = int(match.group(3)) - - return discord.PartialEmoji.with_state( - ctx.bot._connection, - animated=emoji_animated, - name=emoji_name, - id=emoji_id, - ) - - if argument in UNICODE_EMOJIS: - return discord.PartialEmoji.with_state( - ctx.bot._connection, - animated=False, - name=argument, - id=None, - ) - - raise PartialEmojiConversionFailure(argument) - - -class GuildStickerConverter(IDConverter[discord.GuildSticker]): - """Converts to a :class:`~discord.GuildSticker`. - - All lookups are done for the local guild first, if available. If that lookup - fails, then it checks the client's global cache. - - The lookup strategy is as follows (in order): - - 1. Lookup by ID. - 3. Lookup by name - - .. versionadded:: 2.0 - """ - - async def convert(self, ctx: Context, argument: str) -> discord.GuildSticker: - match = self._get_id_match(argument) - result = None - bot = ctx.bot - guild = ctx.guild - - if match is None: - # Try to get the sticker by name. Try local guild first. - if guild: - result = discord.utils.find(lambda s: s.name == argument, guild.stickers) - - if result is None: - result = discord.utils.find(lambda s: s.name == argument, await bot.get_stickers()) - else: - sticker_id = int(match.group(1)) - - # Try to look up sticker by id. - result = await bot.get_sticker(sticker_id) - - if result is None: - raise GuildStickerNotFound(argument) - - return result - - -class clean_content(Converter[str]): - """Converts the argument to mention scrubbed version of - said content. - - This behaves similarly to :attr:`~discord.Message.clean_content`. - - Attributes - ---------- - fix_channel_mentions: :class:`bool` - Whether to clean channel mentions. - use_nicknames: :class:`bool` - Whether to use nicknames when transforming mentions. - escape_markdown: :class:`bool` - Whether to also escape special markdown characters. - remove_markdown: :class:`bool` - Whether to also remove special markdown characters. This option is not supported with ``escape_markdown`` - - .. versionadded:: 1.7 - """ - - def __init__( - self, - *, - fix_channel_mentions: bool = False, - use_nicknames: bool = True, - escape_markdown: bool = False, - remove_markdown: bool = False, - ) -> None: - self.fix_channel_mentions = fix_channel_mentions - self.use_nicknames = use_nicknames - self.escape_markdown = escape_markdown - self.remove_markdown = remove_markdown - - async def convert(self, ctx: Context, argument: str) -> str: - msg = ctx.message - - if ctx.guild: - - def resolve_member(id: int) -> str: - m = ( - None if msg is None else discord.utils.find(lambda e: e.id == id, msg.mentions) - ) or ctx.guild.get_member(id) - return f"@{m.display_name if self.use_nicknames else m.name}" if m else "@deleted-user" - - def resolve_role(id: int) -> str: - r = ( - None if msg is None else discord.utils.find(lambda e: e.id == id, msg.mentions) - ) or ctx.guild.get_role(id) - return f"@{r.name}" if r else "@deleted-role" - - else: - - def resolve_member(id: int) -> str: - # TODO: how tf to fix this??? - m = ( - None if msg is None else discord.utils.find(lambda e: e.id == id, msg.mentions) - ) or ctx.bot.get_user(id) - return f"@{m.name}" if m else "@deleted-user" - - def resolve_role(id: int) -> str: - return "@deleted-role" - - if self.fix_channel_mentions and ctx.guild: - - def resolve_channel(id: int) -> str: - c = ctx.guild.get_channel(id) - return f"#{c.name}" if c else "#deleted-channel" - - else: - - def resolve_channel(id: int) -> str: - return f"<#{id}>" - - transforms = { - "@": resolve_member, - "@!": resolve_member, - "#": resolve_channel, - "@&": resolve_role, - } - - def repl(match: re.Match) -> str: - type = match[1] - id = int(match[2]) - transformed = transforms[type](id) - return transformed - - result = re.sub(r"<(@[!&]?|#)([0-9]{15,20})>", repl, argument) - if self.escape_markdown: - result = discord.utils.escape_markdown(result) - elif self.remove_markdown: - result = discord.utils.remove_markdown(result) - - # Completely ensure no mentions escape: - return discord.utils.escape_mentions(result) - - -class Greedy(List[T]): - r"""A special converter that greedily consumes arguments until it can't. - As a consequence of this behaviour, most input errors are silently discarded, - since it is used as an indicator of when to stop parsing. - - When a parser error is met the greedy converter stops converting, undoes the - internal string parsing routine, and continues parsing regularly. - - For example, in the following code: - - .. code-block:: python3 - - @commands.command() - async def test(ctx, numbers: Greedy[int], reason: str): - await ctx.send("numbers: {}, reason: {}".format(numbers, reason)) - - An invocation of ``[p]test 1 2 3 4 5 6 hello`` would pass ``numbers`` with - ``[1, 2, 3, 4, 5, 6]`` and ``reason`` with ``hello``\. - - For more information, check :ref:`ext_commands_special_converters`. - """ - - __slots__ = ("converter",) - - def __init__(self, *, converter: T): - self.converter = converter - - def __repr__(self): - converter = getattr(self.converter, "__name__", repr(self.converter)) - return f"Greedy[{converter}]" - - def __class_getitem__(cls, params: tuple[T] | T) -> Greedy[T]: - if not isinstance(params, tuple): - params = (params,) - if len(params) != 1: - raise TypeError("Greedy[...] only takes a single argument") - converter = params[0] - - origin = getattr(converter, "__origin__", None) - args = getattr(converter, "__args__", ()) - - if not (callable(converter) or isinstance(converter, Converter) or origin is not None): - raise TypeError("Greedy[...] expects a type or a Converter instance.") - - if converter in (str, type(None)) or origin is Greedy: - raise TypeError(f"Greedy[{converter.__name__}] is invalid.") - - if origin is Union and type(None) in args: - raise TypeError(f"Greedy[{converter!r}] is invalid.") - - return cls(converter=converter) - - -def _convert_to_bool(argument: str) -> bool: - lowered = argument.lower() - if lowered in ("yes", "y", "true", "t", "1", "enable", "on"): - return True - elif lowered in ("no", "n", "false", "f", "0", "disable", "off"): - return False - else: - raise BadBoolArgument(lowered) - - -def get_converter(param: inspect.Parameter) -> Any: - converter = param.annotation - if converter is param.empty: - if param.default is not param.empty: - converter = str if param.default is None else type(param.default) - else: - converter = str - return converter - - -_GenericAlias = type(List[T]) - - -def is_generic_type(tp: Any, *, _GenericAlias: type = _GenericAlias) -> bool: - return isinstance(tp, type) and issubclass(tp, Generic) or isinstance(tp, _GenericAlias) # type: ignore - - -CONVERTER_MAPPING: dict[type[Any], Any] = { - discord.Object: ObjectConverter, - discord.Member: MemberConverter, - discord.User: UserConverter, - discord.Message: MessageConverter, - discord.PartialMessage: PartialMessageConverter, - discord.TextChannel: TextChannelConverter, - discord.Invite: InviteConverter, - discord.Guild: GuildConverter, - discord.Role: RoleConverter, - discord.Game: GameConverter, - discord.Colour: ColourConverter, - discord.VoiceChannel: VoiceChannelConverter, - discord.StageChannel: StageChannelConverter, - discord.GuildEmoji: EmojiConverter, - discord.PartialEmoji: PartialEmojiConverter, - discord.CategoryChannel: CategoryChannelConverter, - discord.ForumChannel: ForumChannelConverter, - discord.Thread: ThreadConverter, - discord.channel.GuildChannel: GuildChannelConverter, - discord.GuildSticker: GuildStickerConverter, -} - - -async def _actual_conversion(ctx: Context, converter, argument: str, param: inspect.Parameter): - if converter is bool: - return _convert_to_bool(argument) - - try: - module = converter.__module__ - except AttributeError: - pass - else: - if module is not None and (module.startswith("discord.") and not module.endswith("converter")): - converter = CONVERTER_MAPPING.get(converter, converter) - - try: - if inspect.isclass(converter) and issubclass(converter, Converter): - if inspect.ismethod(converter.convert): - return await converter.convert(ctx, argument) - else: - return await converter().convert(ctx, argument) - elif isinstance(converter, Converter): - return await converter.convert(ctx, argument) - except CommandError: - raise - except Exception as exc: - raise ConversionError(converter, exc) from exc - - try: - return converter(argument) - except CommandError: - raise - except Exception as exc: - try: - name = converter.__name__ - except AttributeError: - name = converter.__class__.__name__ - - raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc - - -async def run_converters(ctx: Context, converter, argument: str | None, param: inspect.Parameter): - """|coro| - - Runs converters for a given converter, argument, and parameter. - - This function does the same work that the library does under the hood. - - .. versionadded:: 2.0 - - Parameters - ---------- - ctx: :class:`Context` - The invocation context to run the converters under. - converter: Any - The converter to run, this corresponds to the annotation in the function. - argument: Optional[:class:`str`] - The argument to convert to. - param: :class:`inspect.Parameter` - The parameter being converted. This is mainly for error reporting. - - Returns - ------- - Any - The resulting conversion. - - Raises - ------ - CommandError - The converter failed to convert. - """ - origin = getattr(converter, "__origin__", None) - - if origin is Union: - errors = [] - _NoneType = type(None) - union_args = converter.__args__ - for conv in union_args: - # if we got to this part in the code, then the previous conversions have failed, so - # we should just undo the view, return the default, and allow parsing to continue - # with the other parameters - if conv is _NoneType and param.kind != param.VAR_POSITIONAL: - ctx.view.undo() - return None if param.default is param.empty else param.default - - try: - value = await run_converters(ctx, conv, argument, param) - except CommandError as exc: - errors.append(exc) - else: - return value - - # if we're here, then we failed all the converters - raise BadUnionArgument(param, union_args, errors) - - if origin is Literal: - errors = [] - conversions = {} - literal_args = converter.__args__ - for literal in literal_args: - literal_type = type(literal) - try: - value = conversions[literal_type] - except KeyError: - try: - value = await _actual_conversion(ctx, literal_type, argument, param) - except CommandError as exc: - errors.append(exc) - conversions[literal_type] = object() - continue - else: - conversions[literal_type] = value - - if value == literal: - return value - - # if we're here, then we failed to match all the literals - raise BadLiteralArgument(param, literal_args, errors) - - # This must be the last if-clause in the chain of origin checking - # Nearly every type is a generic type within the typing library - # So care must be taken to make sure a more specialised origin handle - # isn't overwritten by the widest if clause - if origin is not None and is_generic_type(converter): - converter = origin - - return await _actual_conversion(ctx, converter, argument, param) diff --git a/discord/ext/commands/cooldowns.py b/discord/ext/commands/cooldowns.py deleted file mode 100644 index ab4361a64e..0000000000 --- a/discord/ext/commands/cooldowns.py +++ /dev/null @@ -1,397 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -import asyncio -import time -from collections import deque -from typing import TYPE_CHECKING, Any, Callable, Deque, TypeVar - -import discord.abc -import discord.channel.base -from discord.enums import Enum - -from ...abc import PrivateChannel -from .errors import MaxConcurrencyReached - -if TYPE_CHECKING: - from ...message import Message - -__all__ = ( - "BucketType", - "Cooldown", - "CooldownMapping", - "DynamicCooldownMapping", - "MaxConcurrency", -) - -C = TypeVar("C", bound="CooldownMapping") -MC = TypeVar("MC", bound="MaxConcurrency") - - -class BucketType(Enum): - default = 0 - user = 1 - guild = 2 - channel = 3 - member = 4 - category = 5 - role = 6 - - def get_key(self, msg: Message) -> Any: - if self is BucketType.user: - return msg.author.id - elif self is BucketType.guild: - return (msg.guild or msg.author).id - elif self is BucketType.channel: - return msg.channel.id - elif self is BucketType.member: - return (msg.guild and msg.guild.id), msg.author.id - elif self is BucketType.category: - return ( - msg.channel.category.id - if isinstance(msg.channel, discord.channel.base.GuildChannel) and msg.channel.category - else msg.channel.id - ) - elif self is BucketType.role: - # we return the channel id of a private-channel as there are only roles in guilds - # and that yields the same result as for a guild with only the @everyone role - # NOTE: PrivateChannel doesn't actually have an id attribute, but we assume we are - # receiving a DMChannel or GroupChannel which inherit from PrivateChannel and do - return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id # type: ignore - - def __call__(self, msg: Message) -> Any: - return self.get_key(msg) - - -class Cooldown: - """Represents a cooldown for a command. - - Attributes - ---------- - rate: :class:`int` - The total number of tokens available per :attr:`per` seconds. - per: :class:`float` - The length of the cooldown period in seconds. - """ - - __slots__ = ("rate", "per", "_window", "_tokens", "_last") - - def __init__(self, rate: float, per: float) -> None: - self.rate: int = int(rate) - self.per: float = float(per) - self._window: float = 0.0 - self._tokens: int = self.rate - self._last: float = 0.0 - - def get_tokens(self, current: float | None = None) -> int: - """Returns the number of available tokens before rate limiting is applied. - - Parameters - ---------- - current: Optional[:class:`float`] - The time in seconds since Unix epoch to calculate tokens at. - If not supplied then :func:`time.time()` is used. - - Returns - ------- - :class:`int` - The number of tokens available before the cooldown is to be applied. - """ - if not current: - current = time.time() - - tokens = self._tokens - - if current > self._window + self.per: - tokens = self.rate - return tokens - - def get_retry_after(self, current: float | None = None) -> float: - """Returns the time in seconds until the cooldown will be reset. - - Parameters - ---------- - current: Optional[:class:`float`] - The current time in seconds since Unix epoch. - If not supplied, then :func:`time.time()` is used. - - Returns - ------- - :class:`float` - The number of seconds to wait before this cooldown will be reset. - """ - current = current or time.time() - tokens = self.get_tokens(current) - - if tokens == 0: - return self.per - (current - self._window) - - return 0.0 - - def update_rate_limit(self, current: float | None = None) -> float | None: - """Updates the cooldown rate limit. - - Parameters - ---------- - current: Optional[:class:`float`] - The time in seconds since Unix epoch to update the rate limit at. - If not supplied, then :func:`time.time()` is used. - - Returns - ------- - Optional[:class:`float`] - The retry-after time in seconds if rate limited. - """ - current = current or time.time() - self._last = current - - self._tokens = self.get_tokens(current) - - # first token used means that we start a new rate limit window - if self._tokens == self.rate: - self._window = current - - # check if we are rate limited - if self._tokens == 0: - return self.per - (current - self._window) - - # we're not so decrement our tokens - self._tokens -= 1 - - def reset(self) -> None: - """Reset the cooldown to its initial state.""" - self._tokens = self.rate - self._last = 0.0 - - def copy(self) -> Cooldown: - """Creates a copy of this cooldown. - - Returns - ------- - :class:`Cooldown` - A new instance of this cooldown. - """ - return Cooldown(self.rate, self.per) - - def __repr__(self) -> str: - return f"" - - -class CooldownMapping: - def __init__( - self, - original: Cooldown | None, - type: Callable[[Message], Any], - ) -> None: - if not callable(type): - raise TypeError("Cooldown type must be a BucketType or callable") - - self._cache: dict[Any, Cooldown] = {} - self._cooldown: Cooldown | None = original - self._type: Callable[[Message], Any] = type - - def copy(self) -> CooldownMapping: - ret = CooldownMapping(self._cooldown, self._type) - ret._cache = self._cache.copy() - return ret - - @property - def valid(self) -> bool: - return self._cooldown is not None - - @property - def type(self) -> Callable[[Message], Any]: - return self._type - - @classmethod - def from_cooldown(cls: type[C], rate, per, type) -> C: - return cls(Cooldown(rate, per), type) - - def _bucket_key(self, msg: Message) -> Any: - return self._type(msg) - - def _verify_cache_integrity(self, current: float | None = None) -> None: - # we want to delete all cache objects that haven't been used - # in a cooldown window. e.g. if we have a command that has a - # cooldown of 60s, and it has not been used in 60s then that key should be deleted - current = current or time.time() - dead_keys = [k for k, v in self._cache.items() if current > v._last + v.per] - for k in dead_keys: - del self._cache[k] - - def create_bucket(self, message: Message) -> Cooldown: - return self._cooldown.copy() # type: ignore - - def get_bucket(self, message: Message, current: float | None = None) -> Cooldown: - if self._type is BucketType.default: - return self._cooldown # type: ignore - - self._verify_cache_integrity(current) - key = self._bucket_key(message) - if key not in self._cache: - bucket = self.create_bucket(message) - if bucket is not None: - self._cache[key] = bucket - else: - bucket = self._cache[key] - - return bucket - - def update_rate_limit(self, message: Message, current: float | None = None) -> float | None: - bucket = self.get_bucket(message, current) - return bucket.update_rate_limit(current) - - -class DynamicCooldownMapping(CooldownMapping): - def __init__(self, factory: Callable[[Message], Cooldown], type: Callable[[Message], Any]) -> None: - super().__init__(None, type) - self._factory: Callable[[Message], Cooldown] = factory - - def copy(self) -> DynamicCooldownMapping: - ret = DynamicCooldownMapping(self._factory, self._type) - ret._cache = self._cache.copy() - return ret - - @property - def valid(self) -> bool: - return True - - def create_bucket(self, message: Message) -> Cooldown: - return self._factory(message) - - -class _Semaphore: - """This class is a version of a semaphore. - - If you're wondering why asyncio.Semaphore isn't being used, - it's because it doesn't expose the internal value. This internal - value is necessary because I need to support both `wait=True` and - `wait=False`. - - An asyncio.Queue could have been used to do this as well -- but it is - not as efficient since internally that uses two queues and is a bit - overkill for what is basically a counter. - """ - - __slots__ = ("value", "loop", "_waiters") - - def __init__(self, number: int) -> None: - self.value: int = number - self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() - self._waiters: Deque[asyncio.Future] = deque() - - def __repr__(self) -> str: - return f"<_Semaphore value={self.value} waiters={len(self._waiters)}>" - - def locked(self) -> bool: - return self.value == 0 - - def is_active(self) -> bool: - return len(self._waiters) > 0 - - def wake_up(self) -> None: - while self._waiters: - future = self._waiters.popleft() - if not future.done(): - future.set_result(None) - return - - async def acquire(self, *, wait: bool = False) -> bool: - if not wait and self.value <= 0: - # signal that we're not acquiring - return False - - while self.value <= 0: - future = self.loop.create_future() - self._waiters.append(future) - try: - await future - except: - future.cancel() - if self.value > 0 and not future.cancelled(): - self.wake_up() - raise - - self.value -= 1 - return True - - def release(self) -> None: - self.value += 1 - self.wake_up() - - -class MaxConcurrency: - __slots__ = ("number", "per", "wait", "_mapping") - - def __init__(self, number: int, *, per: BucketType, wait: bool) -> None: - self._mapping: dict[Any, _Semaphore] = {} - self.per: BucketType = per - self.number: int = number - self.wait: bool = wait - - if number <= 0: - raise ValueError("max_concurrency 'number' cannot be less than 1") - - if not isinstance(per, BucketType): - raise TypeError(f"max_concurrency 'per' must be of type BucketType not {type(per)!r}") - - def copy(self: MC) -> MC: - return self.__class__(self.number, per=self.per, wait=self.wait) - - def __repr__(self) -> str: - return f"" - - def get_key(self, message: Message) -> Any: - return self.per.get_key(message) - - async def acquire(self, message: Message) -> None: - key = self.get_key(message) - - try: - sem = self._mapping[key] - except KeyError: - self._mapping[key] = sem = _Semaphore(self.number) - - acquired = await sem.acquire(wait=self.wait) - if not acquired: - raise MaxConcurrencyReached(self.number, self.per) - - async def release(self, message: Message) -> None: - # Technically there's no reason for this function to be async - # But it might be more useful in the future - key = self.get_key(message) - - try: - sem = self._mapping[key] - except KeyError: - # ...? peculiar - return - else: - sem.release() - - if sem.value >= self.number and not sem.is_active(): - del self._mapping[key] diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py deleted file mode 100644 index 108cd16aa6..0000000000 --- a/discord/ext/commands/core.py +++ /dev/null @@ -1,2394 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Pycord Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -import asyncio -import datetime -import functools -import inspect -import types -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Generator, - Generic, - Literal, - TypeVar, - Union, - overload, -) - -import discord -from discord import utils -from discord.utils import Undefined -from discord.utils.private import async_all, evaluate_annotation, maybe_awaitable - -from ...commands import ( - ApplicationCommand, - Option, - _BaseCommand, - message_command, - slash_command, - user_command, -) -from ...enums import ChannelType -from ...errors import * -from .cog import Cog -from .context import Context -from .converter import Greedy, get_converter, run_converters -from .cooldowns import ( - BucketType, - Cooldown, - CooldownMapping, - DynamicCooldownMapping, - MaxConcurrency, -) -from .errors import * - -if TYPE_CHECKING: - from typing_extensions import Concatenate, ParamSpec, TypeGuard - - from discord.message import Message - - from ._types import Check, Coro, CoroFunc, Error, Hook - - -__all__ = ( - "Command", - "Group", - "GroupMixin", - "command", - "group", - "has_role", - "has_permissions", - "has_any_role", - "check", - "check_any", - "before_invoke", - "after_invoke", - "bot_has_role", - "bot_has_permissions", - "bot_has_any_role", - "cooldown", - "dynamic_cooldown", - "max_concurrency", - "dm_only", - "guild_only", - "is_owner", - "is_nsfw", - "has_guild_permissions", - "bot_has_guild_permissions", - "slash_command", - "user_command", - "message_command", -) - -MISSING: Any = discord.utils.MISSING - -T = TypeVar("T") -CogT = TypeVar("CogT", bound="Cog") -CommandT = TypeVar("CommandT", bound="Command") -ContextT = TypeVar("ContextT", bound="Context") -# CHT = TypeVar('CHT', bound='Check') -GroupT = TypeVar("GroupT", bound="Group") -HookT = TypeVar("HookT", bound="Hook") -ErrorT = TypeVar("ErrorT", bound="Error") - -if TYPE_CHECKING: - P = ParamSpec("P") -else: - P = TypeVar("P") - - -def unwrap_function(function: Callable[..., Any]) -> Callable[..., Any]: - partial = functools.partial - while True: - if hasattr(function, "__wrapped__"): - function = function.__wrapped__ - elif isinstance(function, partial): - function = function.func - else: - return function - - -def get_signature_parameters(function: Callable[..., Any], globalns: dict[str, Any]) -> dict[str, inspect.Parameter]: - signature = inspect.signature(function) - params = {} - cache: dict[str, Any] = {} - for name, parameter in signature.parameters.items(): - annotation = parameter.annotation - if annotation is parameter.empty: - params[name] = parameter - continue - if annotation is None: - params[name] = parameter.replace(annotation=type(None)) - continue - - annotation = evaluate_annotation(annotation, globalns, globalns, cache) - if annotation is Greedy: - raise TypeError("Unparameterized Greedy[...] is disallowed in signature.") - - params[name] = parameter.replace(annotation=annotation) - - return params - - -def wrap_callback(coro): - @functools.wraps(coro) - async def wrapped(*args, **kwargs): - try: - ret = await coro(*args, **kwargs) - except CommandError: - raise - except asyncio.CancelledError: - return - except Exception as exc: - raise CommandInvokeError(exc) from exc - return ret - - return wrapped - - -def hooked_wrapped_callback(command, ctx, coro): - @functools.wraps(coro) - async def wrapped(*args, **kwargs): - try: - ret = await coro(*args, **kwargs) - except CommandError: - ctx.command_failed = True - raise - except asyncio.CancelledError: - ctx.command_failed = True - return - except Exception as exc: - ctx.command_failed = True - raise CommandInvokeError(exc) from exc - finally: - if command._max_concurrency is not None: - await command._max_concurrency.release(ctx) - - await command.call_after_hooks(ctx) - return ret - - return wrapped - - -class _CaseInsensitiveDict(dict): - def __contains__(self, k): - return super().__contains__(k.casefold()) - - def __delitem__(self, k): - return super().__delitem__(k.casefold()) - - def __getitem__(self, k): - return super().__getitem__(k.casefold()) - - def get(self, k, default=None): - return super().get(k.casefold(), default) - - def pop(self, k, default=None): - return super().pop(k.casefold(), default) - - def __setitem__(self, k, v): - super().__setitem__(k.casefold(), v) - - -class Command(_BaseCommand, Generic[CogT, P, T]): - r"""A class that implements the protocol for a bot text command. - - These are not created manually, instead they are created via the - decorator or functional interface. - - Attributes - ----------- - name: :class:`str` - The name of the command. - callback: :ref:`coroutine ` - The coroutine that is executed when the command is called. - help: Optional[:class:`str`] - The long help text for the command. - brief: Optional[:class:`str`] - The short help text for the command. - usage: Optional[:class:`str`] - A replacement for arguments in the default help text. - aliases: Union[List[:class:`str`], Tuple[:class:`str`]] - The list of aliases the command can be invoked under. - enabled: :class:`bool` - A boolean that indicates if the command is currently enabled. - If the command is invoked while it is disabled, then - :exc:`.DisabledCommand` is raised to the :func:`.on_command_error` - event. Defaults to ``True``. - parent: Optional[:class:`Group`] - The parent group that this command belongs to. ``None`` if there - isn't one. - cog: Optional[:class:`Cog`] - The cog that this command belongs to. ``None`` if there isn't one. - checks: List[Callable[[:class:`.Context`], :class:`bool`]] - A list of predicates that verifies if the command could be executed - with the given :class:`.Context` as the sole parameter. If an exception - is necessary to be thrown to signal failure, then one inherited from - :exc:`.CommandError` should be used. Note that if the checks fail then - :exc:`.CheckFailure` exception is raised to the :func:`.on_command_error` - event. - description: :class:`str` - The message prefixed into the default help command. - hidden: :class:`bool` - If ``True``\, the default help command does not show this in the - help output. - rest_is_raw: :class:`bool` - If ``False`` and a keyword-only argument is provided then the keyword - only argument is stripped and handled as if it was a regular argument - that handles :exc:`.MissingRequiredArgument` and default values in a - regular matter rather than passing the rest completely raw. If ``True`` - then the keyword-only argument will pass in the rest of the arguments - in a completely raw matter. Defaults to ``False``. - invoked_subcommand: Optional[:class:`Command`] - The subcommand that was invoked, if any. - require_var_positional: :class:`bool` - If ``True`` and a variadic positional argument is specified, requires - the user to specify at least one argument. Defaults to ``False``. - - .. versionadded:: 1.5 - - ignore_extra: :class:`bool` - If ``True``\, ignores extraneous strings passed to a command if all its - requirements are met (e.g. ``?foo a b c`` when only expecting ``a`` - and ``b``). Otherwise :func:`.on_command_error` and local error handlers - are called with :exc:`.TooManyArguments`. Defaults to ``True``. - cooldown_after_parsing: :class:`bool` - If ``True``\, cooldown processing is done after argument parsing, - which calls converters. If ``False`` then cooldown processing is done - first and then the converters are called second. Defaults to ``False``. - extras: :class:`dict` - A dict of user provided extras to attach to the Command. - - .. note:: - This object may be copied by the library. - - - .. versionadded:: 2.0 - - cooldown: Optional[:class:`Cooldown`] - The cooldown applied when the command is invoked. ``None`` if the command - doesn't have a cooldown. - - .. versionadded:: 2.0 - """ - - __original_kwargs__: dict[str, Any] - - def __new__(cls: type[CommandT], *args: Any, **kwargs: Any) -> CommandT: - # if you're wondering why this is done, it's because we need to ensure - # we have a complete original copy of **kwargs even for classes that - # mess with it by popping before delegating to the subclass __init__. - # In order to do this, we need to control the instance creation and - # inject the original kwargs through __new__ rather than doing it - # inside __init__. - self = super().__new__(cls) - - # we do a shallow copy because it's probably the most common use case. - # this could potentially break if someone modifies a list or something - # while it's in movement, but for now this is the cheapest and - # fastest way to do what we want. - self.__original_kwargs__ = kwargs.copy() - return self - - def __init__( - self, - func: (Callable[Concatenate[CogT, ContextT, P], Coro[T]] | Callable[Concatenate[ContextT, P], Coro[T]]), - **kwargs: Any, - ): - if not asyncio.iscoroutinefunction(func): - raise TypeError("Callback must be a coroutine.") - - name = kwargs.get("name") or func.__name__ - if not isinstance(name, str): - raise TypeError("Name of a command must be a string.") - self.name: str = name - - self.callback = func - self.enabled: bool = kwargs.get("enabled", True) - - help_doc = kwargs.get("help") - if help_doc is not None: - help_doc = inspect.cleandoc(help_doc) - else: - help_doc = inspect.getdoc(func) - if isinstance(help_doc, bytes): - help_doc = help_doc.decode("utf-8") - - self.help: str | None = help_doc - - self.brief: str | None = kwargs.get("brief") - self.usage: str | None = kwargs.get("usage") - self.rest_is_raw: bool = kwargs.get("rest_is_raw", False) - self.aliases: list[str] | tuple[str] = kwargs.get("aliases", []) - self.extras: dict[str, Any] = kwargs.get("extras", {}) - - if not isinstance(self.aliases, (list, tuple)): - raise TypeError("Aliases of a command must be a list or a tuple of strings.") - - self.description: str = inspect.cleandoc(kwargs.get("description", "")) - self.hidden: bool = kwargs.get("hidden", False) - - try: - checks = func.__commands_checks__ - checks.reverse() - except AttributeError: - checks = kwargs.get("checks", []) - - self.checks: list[Check] = checks - - try: - cooldown = func.__commands_cooldown__ - except AttributeError: - cooldown = kwargs.get("cooldown") - - if cooldown is None: - buckets = CooldownMapping(cooldown, BucketType.default) - elif isinstance(cooldown, CooldownMapping): - buckets = cooldown - else: - raise TypeError("Cooldown must be a an instance of CooldownMapping or None.") - self._buckets: CooldownMapping = buckets - - try: - max_concurrency = func.__commands_max_concurrency__ - except AttributeError: - max_concurrency = kwargs.get("max_concurrency") - - self._max_concurrency: MaxConcurrency | None = max_concurrency - - self.require_var_positional: bool = kwargs.get("require_var_positional", False) - self.ignore_extra: bool = kwargs.get("ignore_extra", True) - self.cooldown_after_parsing: bool = kwargs.get("cooldown_after_parsing", False) - self.cog: CogT | None = None - - # bandaid for the fact that sometimes parent can be the bot instance - parent = kwargs.get("parent") - self.parent: GroupMixin | None = parent if isinstance(parent, _BaseCommand) else None # type: ignore - - self._before_invoke: Hook | None = None - try: - before_invoke = func.__before_invoke__ - except AttributeError: - pass - else: - self.before_invoke(before_invoke) - - self._after_invoke: Hook | None = None - try: - after_invoke = func.__after_invoke__ - except AttributeError: - pass - else: - self.after_invoke(after_invoke) - - @property - def callback( - self, - ) -> Callable[Concatenate[CogT, Context, P], Coro[T]] | Callable[Concatenate[Context, P], Coro[T]]: - return self._callback - - @callback.setter - def callback( - self, - function: (Callable[Concatenate[CogT, Context, P], Coro[T]] | Callable[Concatenate[Context, P], Coro[T]]), - ) -> None: - self._callback = function - unwrap = unwrap_function(function) - self.module = unwrap.__module__ - - try: - globalns = unwrap.__globals__ - except AttributeError: - globalns = {} - - self.params = get_signature_parameters(function, globalns) - - def add_check(self, func: Check) -> None: - """Adds a check to the command. - - This is the non-decorator interface to :func:`.check`. - - .. versionadded:: 1.3 - - Parameters - ---------- - func - The function that will be used as a check. - """ - - self.checks.append(func) - - def remove_check(self, func: Check) -> None: - """Removes a check from the command. - - This function is idempotent and will not raise an exception - if the function is not in the command's checks. - - .. versionadded:: 1.3 - - Parameters - ---------- - func - The function to remove from the checks. - """ - - try: - self.checks.remove(func) - except ValueError: - pass - - def update(self, **kwargs: Any) -> None: - """Updates :class:`Command` instance with updated attribute. - - This works similarly to the :func:`.command` decorator in terms - of parameters in that they are passed to the :class:`Command` or - subclass constructors, sans the name and callback. - """ - self.__init__(self.callback, **dict(self.__original_kwargs__, **kwargs)) - - async def __call__(self, context: Context, *args: P.args, **kwargs: P.kwargs) -> T: - """|coro| - - Calls the internal callback that the command holds. - - .. note:: - - This bypasses all mechanisms -- including checks, converters, - invoke hooks, cooldowns, etc. You must take care to pass - the proper arguments and types to this function. - - .. versionadded:: 1.3 - """ - if self.cog is not None: - return await self.callback(self.cog, context, *args, **kwargs) # type: ignore - else: - return await self.callback(context, *args, **kwargs) # type: ignore - - def _ensure_assignment_on_copy(self, other: CommandT) -> CommandT: - other._before_invoke = self._before_invoke - other._after_invoke = self._after_invoke - if self.checks != other.checks: - other.checks = self.checks.copy() - if self._buckets.valid and not other._buckets.valid: - other._buckets = self._buckets.copy() - if self._max_concurrency != other._max_concurrency: - # _max_concurrency won't be None at this point - other._max_concurrency = self._max_concurrency.copy() # type: ignore - - try: - other.on_error = self.on_error - except AttributeError: - pass - return other - - def copy(self: CommandT) -> CommandT: - """Creates a copy of this command. - - Returns - ------- - :class:`Command` - A new instance of this command. - """ - ret = self.__class__(self.callback, **self.__original_kwargs__) - return self._ensure_assignment_on_copy(ret) - - def _update_copy(self: CommandT, kwargs: dict[str, Any]) -> CommandT: - if kwargs: - kw = kwargs.copy() - kw.update(self.__original_kwargs__) - copy = self.__class__(self.callback, **kw) - return self._ensure_assignment_on_copy(copy) - else: - return self.copy() - - async def dispatch_error(self, ctx: Context, error: Exception) -> None: - ctx.command_failed = True - cog = self.cog - try: - coro = self.on_error - except AttributeError: - pass - else: - injected = wrap_callback(coro) - if cog is not None: - await injected(cog, ctx, error) - else: - await injected(ctx, error) - - try: - if cog is not None: - local = Cog._get_overridden_method(cog.cog_command_error) - if local is not None: - wrapped = wrap_callback(local) - await wrapped(ctx, error) - finally: - ctx.bot.dispatch("command_error", ctx, error) - - async def transform(self, ctx: Context, param: inspect.Parameter) -> Any: - if isinstance(param.annotation, Option): - default = param.annotation.default - required = param.annotation.required - else: - default = param.default - required = default is param.empty - - converter = get_converter(param) - consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw - view = ctx.view - view.skip_ws() - - # The greedy converter is simple -- it keeps going until it fails in which case, - # it undoes the view ready for the next parameter to use instead - if isinstance(converter, Greedy): - if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY): - return await self._transform_greedy_pos(ctx, param, required, converter.converter) - elif param.kind == param.VAR_POSITIONAL: - return await self._transform_greedy_var_pos(ctx, param, converter.converter) - else: - # if we're here, then it's a KEYWORD_ONLY param type - # since this is mostly useless, we'll helpfully transform Greedy[X] - # into just X and do the parsing that way. - converter = converter.converter - - if view.eof: - if param.kind == param.VAR_POSITIONAL: - raise RuntimeError() # break the loop - if required: - if self._is_typing_optional(param.annotation): - return None - if hasattr(converter, "__commands_is_flag__") and converter._can_be_constructible(): - return await converter._construct_default(ctx) - raise MissingRequiredArgument(param) - return default - - previous = view.index - if consume_rest_is_special: - argument = view.read_rest().strip() - else: - try: - argument = view.get_quoted_word() - except ArgumentParsingError as exc: - if not self._is_typing_optional(param.annotation): - raise exc - view.index = previous - return None - view.previous = previous - - # type-checker fails to narrow argument - return await run_converters(ctx, converter, argument, param) # type: ignore - - async def _transform_greedy_pos( - self, ctx: Context, param: inspect.Parameter, required: bool, converter: Any - ) -> Any: - view = ctx.view - result = [] - while not view.eof: - # for use with a manual undo - previous = view.index - - view.skip_ws() - try: - argument = view.get_quoted_word() - value = await run_converters(ctx, converter, argument, param) # type: ignore - except (CommandError, ArgumentParsingError): - view.index = previous - break - else: - result.append(value) - - if not result and not required: - return param.default - return result - - async def _transform_greedy_var_pos(self, ctx: Context, param: inspect.Parameter, converter: Any) -> Any: - view = ctx.view - previous = view.index - try: - argument = view.get_quoted_word() - value = await run_converters(ctx, converter, argument, param) # type: ignore - except (CommandError, ArgumentParsingError): - view.index = previous - raise RuntimeError() from None # break loop - else: - return value - - @property - def clean_params(self) -> dict[str, inspect.Parameter]: - """Dict[:class:`str`, :class:`inspect.Parameter`]: - Retrieves the parameter dictionary without the context or self parameters. - - Useful for inspecting signature. - """ - result = self.params.copy() - if self.cog is not None: - # first parameter is self - try: - del result[next(iter(result))] - except StopIteration: - raise ValueError("missing 'self' parameter") from None - - try: - # first/second parameter is context - del result[next(iter(result))] - except StopIteration: - raise ValueError("missing 'context' parameter") from None - - return result - - @property - def full_parent_name(self) -> str: - """Retrieves the fully qualified parent command name. - - This the base command name required to execute it. For example, - in ``?one two three`` the parent name would be ``one two``. - """ - entries = [] - command = self - # command.parent is type-hinted as GroupMixin some attributes are resolved via MRO - while command.parent is not None: # type: ignore - command = command.parent # type: ignore - entries.append(command.name) # type: ignore - - return " ".join(reversed(entries)) - - @property - def parents(self) -> list[Group]: - """Retrieves the parents of this command. - - If the command has no parents then it returns an empty :class:`list`. - - For example in commands ``?a b c test``, the parents are ``[c, b, a]``. - - .. versionadded:: 1.1 - """ - entries = [] - command = self - while command.parent is not None: # type: ignore - command = command.parent # type: ignore - entries.append(command) - - return entries - - @property - def root_parent(self) -> Group | None: - """Retrieves the root parent of this command. - - If the command has no parents then it returns ``None``. - - For example in commands ``?a b c test``, the root parent is ``a``. - """ - if not self.parent: - return None - return self.parents[-1] - - @property - def qualified_name(self) -> str: - """Retrieves the fully qualified command name. - - This is the full parent name with the command name as well. - For example, in ``?one two three`` the qualified name would be - ``one two three``. - """ - - parent = self.full_parent_name - if parent: - return f"{parent} {self.name}" - else: - return self.name - - def __str__(self) -> str: - return self.qualified_name - - async def _parse_arguments(self, ctx: Context) -> None: - ctx.args = [ctx] if self.cog is None else [self.cog, ctx] - ctx.kwargs = {} - args = ctx.args - kwargs = ctx.kwargs - - view = ctx.view - iterator = iter(self.params.items()) - - if self.cog is not None: - # we have 'self' as the first parameter so just advance - # the iterator and resume parsing - try: - next(iterator) - except StopIteration: - raise discord.ClientException( - f'Callback for {self.name} command is missing "self" parameter.' - ) from None - - # next we have the 'ctx' as the next parameter - try: - next(iterator) - except StopIteration: - raise discord.ClientException(f'Callback for {self.name} command is missing "ctx" parameter.') from None - - for name, param in iterator: - ctx.current_parameter = param - if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY): - transformed = await self.transform(ctx, param) - args.append(transformed) - elif param.kind == param.KEYWORD_ONLY: - # kwarg only param denotes "consume rest" semantics - if self.rest_is_raw: - converter = get_converter(param) - argument = view.read_rest() - kwargs[name] = await run_converters(ctx, converter, argument, param) - else: - kwargs[name] = await self.transform(ctx, param) - break - elif param.kind == param.VAR_POSITIONAL: - if view.eof and self.require_var_positional: - raise MissingRequiredArgument(param) - while not view.eof: - try: - transformed = await self.transform(ctx, param) - args.append(transformed) - except RuntimeError: - break - - if not self.ignore_extra and not view.eof: - raise TooManyArguments(f"Too many arguments passed to {self.qualified_name}") - - async def call_before_hooks(self, ctx: Context) -> None: - # now that we're done preparing we can call the pre-command hooks - # first, call the command local hook: - cog = self.cog - if self._before_invoke is not None: - # should be cog if @commands.before_invoke is used - instance = getattr(self._before_invoke, "__self__", cog) - # __self__ only exists for methods, not functions - # however, if @command.before_invoke is used, it will be a function - if instance: - await self._before_invoke(instance, ctx) # type: ignore - else: - await self._before_invoke(ctx) # type: ignore - - # call the cog local hook if applicable: - if cog is not None: - hook = Cog._get_overridden_method(cog.cog_before_invoke) - if hook is not None: - await hook(ctx) - - # call the bot global hook if necessary - hook = ctx.bot._before_invoke - if hook is not None: - await hook(ctx) - - async def call_after_hooks(self, ctx: Context) -> None: - cog = self.cog - if self._after_invoke is not None: - instance = getattr(self._after_invoke, "__self__", cog) - if instance: - await self._after_invoke(instance, ctx) # type: ignore - else: - await self._after_invoke(ctx) # type: ignore - - # call the cog local hook if applicable: - if cog is not None: - hook = Cog._get_overridden_method(cog.cog_after_invoke) - if hook is not None: - await hook(ctx) - - hook = ctx.bot._after_invoke - if hook is not None: - await hook(ctx) - - def _prepare_cooldowns(self, ctx: Context) -> None: - if self._buckets.valid: - dt = ctx.message.edited_at or ctx.message.created_at - current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() - bucket = self._buckets.get_bucket(ctx.message, current) - if bucket is not None: - retry_after = bucket.update_rate_limit(current) - if retry_after: - raise CommandOnCooldown(bucket, retry_after, self._buckets.type) # type: ignore - - async def prepare(self, ctx: Context) -> None: - ctx.command = self - - if not await self.can_run(ctx): - raise CheckFailure(f"The check functions for command {self.qualified_name} failed.") - - if self._max_concurrency is not None: - # For this application, context can be duck-typed as a Message - await self._max_concurrency.acquire(ctx) # type: ignore - - try: - if self.cooldown_after_parsing: - await self._parse_arguments(ctx) - self._prepare_cooldowns(ctx) - else: - self._prepare_cooldowns(ctx) - await self._parse_arguments(ctx) - - await self.call_before_hooks(ctx) - except: - if self._max_concurrency is not None: - await self._max_concurrency.release(ctx) # type: ignore - raise - - @property - def cooldown(self) -> Cooldown | None: - return self._buckets._cooldown - - def is_on_cooldown(self, ctx: Context) -> bool: - """Checks whether the command is currently on cooldown. - - Parameters - ---------- - ctx: :class:`.Context` - The invocation context to use when checking the command's cooldown status. - - Returns - ------- - :class:`bool` - A boolean indicating if the command is on cooldown. - """ - if not self._buckets.valid: - return False - - bucket = self._buckets.get_bucket(ctx.message) - dt = ctx.message.edited_at or ctx.message.created_at - current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() - return bucket.get_tokens(current) == 0 - - def reset_cooldown(self, ctx: Context) -> None: - """Resets the cooldown on this command. - - Parameters - ---------- - ctx: :class:`.Context` - The invocation context to reset the cooldown under. - """ - if self._buckets.valid: - bucket = self._buckets.get_bucket(ctx.message) - bucket.reset() - - def get_cooldown_retry_after(self, ctx: Context) -> float: - """Retrieves the amount of seconds before this command can be tried again. - - .. versionadded:: 1.4 - - Parameters - ---------- - ctx: :class:`.Context` - The invocation context to retrieve the cooldown from. - - Returns - ------- - :class:`float` - The amount of time left on this command's cooldown in seconds. - If this is ``0.0`` then the command isn't on cooldown. - """ - if self._buckets.valid: - bucket = self._buckets.get_bucket(ctx.message) - dt = ctx.message.edited_at or ctx.message.created_at - current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() - return bucket.get_retry_after(current) - - return 0.0 - - async def invoke(self, ctx: Context) -> None: - await self.prepare(ctx) - - # terminate the invoked_subcommand chain. - # since we're in a regular command (and not a group) then - # the invoked subcommand is None. - ctx.invoked_subcommand = None - ctx.subcommand_passed = None - injected = hooked_wrapped_callback(self, ctx, self.callback) - await injected(*ctx.args, **ctx.kwargs) - - async def reinvoke(self, ctx: Context, *, call_hooks: bool = False) -> None: - ctx.command = self - await self._parse_arguments(ctx) - - if call_hooks: - await self.call_before_hooks(ctx) - - ctx.invoked_subcommand = None - try: - await self.callback(*ctx.args, **ctx.kwargs) # type: ignore - except: - ctx.command_failed = True - raise - finally: - if call_hooks: - await self.call_after_hooks(ctx) - - def error(self, coro: ErrorT) -> ErrorT: - """A decorator that registers a coroutine as a local error handler. - - A local error handler is an :func:`.on_command_error` event limited to - a single command. However, the :func:`.on_command_error` is still - invoked afterwards as the catch-all. - - Parameters - ---------- - coro: :ref:`coroutine ` - The coroutine to register as the local error handler. - - Raises - ------ - TypeError - The coroutine passed is not actually a coroutine. - """ - - if not asyncio.iscoroutinefunction(coro): - raise TypeError("The error handler must be a coroutine.") - - self.on_error: Error = coro - return coro - - def has_error_handler(self) -> bool: - """Checks whether the command has an error handler registered. - - .. versionadded:: 1.7 - """ - return hasattr(self, "on_error") - - def before_invoke(self, coro: HookT) -> HookT: - """A decorator that registers a coroutine as a pre-invoke hook. - - A pre-invoke hook is called directly before the command is - called. This makes it a useful function to set up database - connections or any type of set up required. - - This pre-invoke hook takes a sole parameter, a :class:`.Context`. - - See :meth:`.Bot.before_invoke` for more info. - - Parameters - ---------- - coro: :ref:`coroutine ` - The coroutine to register as the pre-invoke hook. - - Raises - ------ - TypeError - The coroutine passed is not actually a coroutine. - """ - if not asyncio.iscoroutinefunction(coro): - raise TypeError("The pre-invoke hook must be a coroutine.") - - self._before_invoke = coro - return coro - - def after_invoke(self, coro: HookT) -> HookT: - """A decorator that registers a coroutine as a post-invoke hook. - - A post-invoke hook is called directly after the command is - called. This makes it a useful function to clean-up database - connections or any type of clean up required. - - This post-invoke hook takes a sole parameter, a :class:`.Context`. - - See :meth:`.Bot.after_invoke` for more info. - - Parameters - ---------- - coro: :ref:`coroutine ` - The coroutine to register as the post-invoke hook. - - Raises - ------ - TypeError - The coroutine passed is not actually a coroutine. - """ - if not asyncio.iscoroutinefunction(coro): - raise TypeError("The post-invoke hook must be a coroutine.") - - self._after_invoke = coro - return coro - - @property - def cog_name(self) -> str | None: - """The name of the cog this command belongs to, if any.""" - return type(self.cog).__cog_name__ if self.cog is not None else None - - @property - def short_doc(self) -> str: - """Gets the "short" documentation of a command. - - By default, this is the :attr:`.brief` attribute. - If that lookup leads to an empty string then the first line of the - :attr:`.help` attribute is used instead. - """ - if self.brief is not None: - return self.brief - if self.help is not None: - return self.help.split("\n", 1)[0] - return "" - - def _is_typing_optional(self, annotation: T | T | None) -> TypeGuard[T | None]: - return ( - getattr(annotation, "__origin__", None) is Union or type(annotation) is getattr(types, "UnionType", Union) - ) and type(None) in annotation.__args__ # type: ignore - - @property - def signature(self) -> str: - """Returns a POSIX-like signature useful for help command output.""" - if self.usage is not None: - return self.usage - - params = self.clean_params - if not params: - return "" - - result = [] - for name, param in params.items(): - greedy = isinstance(param.annotation, Greedy) - optional = False # postpone evaluation of if it's an optional argument - - # for typing.Literal[...], typing.Optional[typing.Literal[...]], and Greedy[typing.Literal[...]], the - # parameter signature is a literal list of it's values - annotation = param.annotation.converter if greedy else param.annotation - origin = getattr(annotation, "__origin__", None) - if not greedy and origin is Union: - none_cls = type(None) - union_args = annotation.__args__ - optional = union_args[-1] is none_cls - if len(union_args) == 2 and optional: - annotation = union_args[0] - origin = getattr(annotation, "__origin__", None) - - if origin is Literal: - name = "|".join(f'"{v}"' if isinstance(v, str) else str(v) for v in annotation.__args__) - if param.default is not param.empty: - # We don't want None or '' to trigger the [name=value] case, and instead it should - # do [name] since [name=None] or [name=] are not exactly useful for the user. - should_print = param.default if isinstance(param.default, str) else param.default is not None - if should_print: - result.append(f"[{name}={param.default}]" if not greedy else f"[{name}={param.default}]...") - continue - else: - result.append(f"[{name}]") - - elif param.kind == param.VAR_POSITIONAL: - if self.require_var_positional: - result.append(f"<{name}...>") - else: - result.append(f"[{name}...]") - elif greedy: - result.append(f"[{name}]...") - elif optional: - result.append(f"[{name}]") - else: - result.append(f"<{name}>") - - return " ".join(result) - - async def can_run(self, ctx: Context) -> bool: - """|coro| - - Checks if the command can be executed by checking all the predicates - inside the :attr:`~Command.checks` attribute. This also checks whether the - command is disabled. - - .. versionchanged:: 1.3 - Checks whether the command is disabled or not - - Parameters - ---------- - ctx: :class:`.Context` - The ctx of the command currently being invoked. - - Returns - ------- - :class:`bool` - A boolean indicating if the command can be invoked. - - Raises - ------ - :class:`CommandError` - Any command error that was raised during a check call will be propagated - by this function. - """ - - if not self.enabled: - raise DisabledCommand(f"{self.name} command is disabled") - - original = ctx.command - ctx.command = self - - try: - if not await ctx.bot.can_run(ctx): - raise CheckFailure(f"The global check functions for command {self.qualified_name} failed.") - - cog = self.cog - if cog is not None: - local_check = Cog._get_overridden_method(cog.cog_check) - if local_check is not None: - ret = await maybe_awaitable(local_check, ctx) - if not ret: - return False - - predicates = self.checks - if not predicates: - # since we have no checks, then we just return True. - return True - - return await async_all(predicate(ctx) for predicate in predicates) # type: ignore - finally: - ctx.command = original - - def _set_cog(self, cog): - self.cog = cog - - -class GroupMixin(Generic[CogT]): - """A mixin that implements common functionality for classes that behave - similar to :class:`.Group` and are allowed to register commands. - - Attributes - ---------- - all_commands: :class:`dict` - A mapping of command name to :class:`.Command` - objects. - case_insensitive: :class:`bool` - Whether the commands should be case-insensitive. Defaults to ``False``. - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - case_insensitive = kwargs.get("case_insensitive", False) - self.prefixed_commands: dict[str, Command[CogT, Any, Any]] = _CaseInsensitiveDict() if case_insensitive else {} - self.case_insensitive: bool = case_insensitive - super().__init__(*args, **kwargs) - - @property - def all_commands(self): - # merge app and prefixed commands - if hasattr(self, "_application_commands"): - return {**self._application_commands, **self.prefixed_commands} - return self.prefixed_commands - - @property - def commands(self) -> set[Command[CogT, Any, Any]]: - """A unique set of commands without aliases that are registered.""" - return set(self.prefixed_commands.values()) - - def recursively_remove_all_commands(self) -> None: - for command in self.prefixed_commands.copy().values(): - if isinstance(command, GroupMixin): - command.recursively_remove_all_commands() - self.remove_command(command.name) - - def add_command(self, command: Command[CogT, Any, Any]) -> None: - """Adds a :class:`.Command` into the internal list of commands. - - This is usually not called, instead the :meth:`~.GroupMixin.command` or - :meth:`~.GroupMixin.group` shortcut decorators are used instead. - - .. versionchanged:: 1.4 - Raise :exc:`.CommandRegistrationError` instead of generic :exc:`.ClientException` - - Parameters - ---------- - command: :class:`Command` - The command to add. - - Raises - ------ - :exc:`.CommandRegistrationError` - If the command or its alias is already registered by different command. - TypeError - If the command passed is not a subclass of :class:`.Command`. - """ - - if not isinstance(command, Command): - raise TypeError("The command passed must be a subclass of Command") - - if isinstance(self, Command): - command.parent = self - - if command.name in self.prefixed_commands: - raise CommandRegistrationError(command.name) - - self.prefixed_commands[command.name] = command - for alias in command.aliases: - if alias in self.prefixed_commands: - self.remove_command(command.name) - raise CommandRegistrationError(alias, alias_conflict=True) - self.prefixed_commands[alias] = command - - def remove_command(self, name: str) -> Command[CogT, Any, Any] | None: - """Remove a :class:`.Command` from the internal list - of commands. - - This could also be used as a way to remove aliases. - - Parameters - ---------- - name: :class:`str` - The name of the command to remove. - - Returns - ------- - Optional[:class:`.Command`] - The command that was removed. If the name is not valid then - ``None`` is returned instead. - """ - command = self.prefixed_commands.pop(name, None) - - # does not exist - if command is None: - return None - - if name in command.aliases: - # we're removing an alias, so we don't want to remove the rest - return command - - # we're not removing the alias so let's delete the rest of them. - for alias in command.aliases: - cmd = self.prefixed_commands.pop(alias, None) - # in the case of a CommandRegistrationError, an alias might conflict - # with an already existing command. If this is the case, we want to - # make sure the pre-existing command is not removed. - if cmd is not None and cmd != command: - self.prefixed_commands[alias] = cmd - return command - - def walk_commands(self) -> Generator[Command[CogT, Any, Any]]: - """An iterator that recursively walks through all commands and subcommands. - - .. versionchanged:: 1.4 - Duplicates due to aliases are no longer returned - - Yields - ------ - Union[:class:`.Command`, :class:`.Group`] - A command or group from the internal list of commands. - """ - for command in self.commands: - yield command - if isinstance(command, GroupMixin): - yield from command.walk_commands() - - def get_command(self, name: str) -> Command[CogT, Any, Any] | None: - """Get a :class:`.Command` from the internal list - of commands. - - This could also be used as a way to get aliases. - - The name could be fully qualified (e.g. ``'foo bar'``) will get - the subcommand ``bar`` of the group command ``foo``. If a - subcommand is not found then ``None`` is returned just as usual. - - Parameters - ---------- - name: :class:`str` - The name of the command to get. - - Returns - ------- - Optional[:class:`Command`] - The command that was requested. If not found, returns ``None``. - """ - - # fast path, no space in name. - if " " not in name: - return self.prefixed_commands.get(name) - - names = name.split() - if not names: - return None - obj = self.prefixed_commands.get(names[0]) - if not isinstance(obj, GroupMixin): - return obj - - for name in names[1:]: - try: - obj = obj.prefixed_commands[name] # type: ignore - except (AttributeError, KeyError): - return None - - return obj - - @overload - def command( - self, - name: str = ..., - cls: type[Command[CogT, P, T]] = ..., - *args: Any, - **kwargs: Any, - ) -> Callable[ - [(Callable[Concatenate[CogT, ContextT, P], Coro[T]] | Callable[Concatenate[ContextT, P], Coro[T]])], - Command[CogT, P, T], - ]: ... - - @overload - def command( - self, - name: str = ..., - cls: type[CommandT] = ..., - *args: Any, - **kwargs: Any, - ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], CommandT]: ... - - def command( - self, - name: str | Undefined = MISSING, - cls: type[CommandT] | Undefined = MISSING, - *args: Any, - **kwargs: Any, - ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], CommandT]: - """A shortcut decorator that invokes :func:`.command` and adds it to - the internal command list via :meth:`~.GroupMixin.add_command`. - - Returns - ------- - Callable[..., :class:`Command`] - A decorator that converts the provided method into a Command, adds it to the bot, then returns it. - """ - - def decorator(func: Callable[Concatenate[ContextT, P], Coro[Any]]) -> CommandT: - kwargs.setdefault("parent", self) - result = command(name=name, cls=cls, *args, **kwargs)(func) # noqa: B026 - self.add_command(result) - return result - - return decorator - - @overload - def group( - self, - name: str = ..., - cls: type[Group[CogT, P, T]] = ..., - *args: Any, - **kwargs: Any, - ) -> Callable[ - [(Callable[Concatenate[CogT, ContextT, P], Coro[T]] | Callable[Concatenate[ContextT, P], Coro[T]])], - Group[CogT, P, T], - ]: ... - - @overload - def group( - self, - name: str = ..., - cls: type[GroupT] = ..., - *args: Any, - **kwargs: Any, - ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], GroupT]: ... - - def group( - self, - name: str | Undefined = MISSING, - cls: type[GroupT] | Undefined = MISSING, - *args: Any, - **kwargs: Any, - ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], GroupT]: - """A shortcut decorator that invokes :func:`.group` and adds it to - the internal command list via :meth:`~.GroupMixin.add_command`. - - Returns - ------- - Callable[..., :class:`Group`] - A decorator that converts the provided method into a Group, adds it to the bot, then returns it. - """ - - def decorator(func: Callable[Concatenate[ContextT, P], Coro[Any]]) -> GroupT: - kwargs.setdefault("parent", self) - result = group(name=name, cls=cls, *args, **kwargs)(func) # noqa: B026 - self.add_command(result) - return result - - return decorator - - -class Group(GroupMixin[CogT], Command[CogT, P, T]): - """A class that implements a grouping protocol for commands to be - executed as subcommands. - - This class is a subclass of :class:`.Command` and thus all options - valid in :class:`.Command` are valid in here as well. - - Attributes - ---------- - invoke_without_command: :class:`bool` - Indicates if the group callback should begin parsing and - invocation only if no subcommand was found. Useful for - making it an error handling function to tell the user that - no subcommand was found or to have different functionality - in case no subcommand was found. If this is ``False``, then - the group callback will always be invoked first. This means - that the checks and the parsing dictated by its parameters - will be executed. Defaults to ``False``. - case_insensitive: :class:`bool` - Indicates if the group's commands should be case-insensitive. - Defaults to ``False``. - """ - - def __init__(self, *args: Any, **attrs: Any) -> None: - self.invoke_without_command: bool = attrs.pop("invoke_without_command", False) - super().__init__(*args, **attrs) - - def copy(self: GroupT) -> GroupT: - """Creates a copy of this :class:`Group`. - - Returns - ------- - :class:`Group` - A new instance of this group. - """ - ret = super().copy() - for cmd in self.commands: - ret.add_command(cmd.copy()) - return ret # type: ignore - - async def invoke(self, ctx: Context) -> None: - ctx.invoked_subcommand = None - ctx.subcommand_passed = None - early_invoke = not self.invoke_without_command - if early_invoke: - await self.prepare(ctx) - - view = ctx.view - previous = view.index - view.skip_ws() - trigger = view.get_word() - - if trigger: - ctx.subcommand_passed = trigger - ctx.invoked_subcommand = self.prefixed_commands.get(trigger, None) - - if early_invoke: - injected = hooked_wrapped_callback(self, ctx, self.callback) - await injected(*ctx.args, **ctx.kwargs) - - ctx.invoked_parents.append(ctx.invoked_with) # type: ignore - - if trigger and ctx.invoked_subcommand: - ctx.invoked_with = trigger - await ctx.invoked_subcommand.invoke(ctx) - elif not early_invoke: - # undo the trigger parsing - view.index = previous - view.previous = previous - await super().invoke(ctx) - - async def reinvoke(self, ctx: Context, *, call_hooks: bool = False) -> None: - ctx.invoked_subcommand = None - early_invoke = not self.invoke_without_command - if early_invoke: - ctx.command = self - await self._parse_arguments(ctx) - - if call_hooks: - await self.call_before_hooks(ctx) - - view = ctx.view - previous = view.index - view.skip_ws() - trigger = view.get_word() - - if trigger: - ctx.subcommand_passed = trigger - ctx.invoked_subcommand = self.prefixed_commands.get(trigger, None) - - if early_invoke: - try: - await self.callback(*ctx.args, **ctx.kwargs) # type: ignore - except: - ctx.command_failed = True - raise - finally: - if call_hooks: - await self.call_after_hooks(ctx) - - ctx.invoked_parents.append(ctx.invoked_with) # type: ignore - - if trigger and ctx.invoked_subcommand: - ctx.invoked_with = trigger - await ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks) - elif not early_invoke: - # undo the trigger parsing - view.index = previous - view.previous = previous - await super().reinvoke(ctx, call_hooks=call_hooks) - - -# Decorators - - -@overload # for py 3.10 -def command( - name: str = ..., - cls: type[Command[CogT, P, T]] = ..., - **attrs: Any, -) -> Callable[ - [(Callable[Concatenate[CogT, ContextT, P]] | Coro[T] | Callable[Concatenate[ContextT, P]] | Coro[T])], - Command[CogT, P, T], -]: ... - - -@overload -def command( - name: str = ..., - cls: type[Command[CogT, P, T]] = ..., - **attrs: Any, -) -> Callable[ - [(Callable[Concatenate[CogT, ContextT, P], Coro[T]] | Callable[Concatenate[ContextT, P], Coro[T]])], - Command[CogT, P, T], -]: ... - - -@overload -def command( - name: str = ..., - cls: type[CommandT] = ..., - **attrs: Any, -) -> Callable[ - [(Callable[Concatenate[CogT, ContextT, P], Coro[Any]] | Callable[Concatenate[ContextT, P], Coro[Any]])], - CommandT, -]: ... - - -def command( - name: str | utils.Undefined = MISSING, cls: type[CommandT] | utils.Undefined = MISSING, **attrs: Any -) -> Callable[ - [(Callable[Concatenate[ContextT, P], Coro[Any]] | Callable[Concatenate[CogT, ContextT, P], Coro[T]])], - Command[CogT, P, T] | CommandT, -]: - """A decorator that transforms a function into a :class:`.Command` - or if called with :func:`.group`, :class:`.Group`. - - By default the ``help`` attribute is received automatically from the - docstring of the function and is cleaned up with the use of - ``inspect.cleandoc``. If the docstring is ``bytes``, then it is decoded - into :class:`str` using utf-8 encoding. - - All checks added using the :func:`.check` & co. decorators are added into - the function. There is no way to supply your own checks through this - decorator. - - Parameters - ---------- - name: :class:`str` - The name to create the command with. By default, this uses the - function name unchanged. - cls - The class to construct with. By default, this is :class:`.Command`. - You usually do not change this. - attrs - Keyword arguments to pass into the construction of the class denoted - by ``cls``. - - Raises - ------ - TypeError - If the function is not a coroutine or is already a command. - """ - if cls is MISSING: - cls = Command # type: ignore - - def decorator( - func: (Callable[Concatenate[ContextT, P], Coro[Any]] | Callable[Concatenate[CogT, ContextT, P], Coro[Any]]), - ) -> CommandT: - if isinstance(func, Command): - raise TypeError("Callback is already a command.") - return cls(func, name=name, **attrs) - - return decorator - - -@overload -def group( - name: str = ..., - cls: type[Group[CogT, P, T]] = ..., - **attrs: Any, -) -> Callable[ - [(Callable[Concatenate[CogT, ContextT, P], Coro[T]] | Callable[Concatenate[ContextT, P], Coro[T]])], - Group[CogT, P, T], -]: ... - - -@overload -def group( - name: str = ..., - cls: type[GroupT] = ..., - **attrs: Any, -) -> Callable[ - [(Callable[Concatenate[CogT, ContextT, P], Coro[Any]] | Callable[Concatenate[ContextT, P], Coro[Any]])], - GroupT, -]: ... - - -def group( - name: str | utils.Undefined = MISSING, - cls: type[GroupT] | utils.Undefined = MISSING, - **attrs: Any, -) -> Callable[ - [(Callable[Concatenate[ContextT, P], Coro[Any]] | Callable[Concatenate[CogT, ContextT, P], Coro[T]])], - Group[CogT, P, T] | GroupT, -]: - """A decorator that transforms a function into a :class:`.Group`. - - This is similar to the :func:`.command` decorator but the ``cls`` - parameter is set to :class:`Group` by default. - - .. versionchanged:: 1.1 - The ``cls`` parameter can now be passed. - """ - if cls is MISSING: - cls = Group # type: ignore - return command(name=name, cls=cls, **attrs) # type: ignore - - -def check(predicate: Check) -> Callable[[T], T]: - r"""A decorator that adds a check to the :class:`.Command` or its - subclasses. These checks could be accessed via :attr:`.Command.checks`. - - These checks should be predicates that take in a single parameter taking - a :class:`.Context`. If the check returns a ``False``\-like value then - during invocation a :exc:`.CheckFailure` exception is raised and sent to - the :func:`.on_command_error` event. - - If an exception should be thrown in the predicate then it should be a - subclass of :exc:`.CommandError`. Any exception not subclassed from it - will be propagated while those subclassed will be sent to - :func:`.on_command_error`. - - A special attribute named ``predicate`` is bound to the value - returned by this decorator to retrieve the predicate passed to the - decorator. This allows the following introspection and chaining to be done: - - .. code-block:: python3 - - def owner_or_permissions(**perms): - original = commands.has_permissions(**perms).predicate - - async def extended_check(ctx): - if ctx.guild is None: - return False - return ctx.guild.owner_id == ctx.author.id or await original(ctx) - - return commands.check(extended_check) - - .. note:: - - The function returned by ``predicate`` is **always** a coroutine, - even if the original function was not a coroutine. - - .. versionchanged:: 1.3 - The ``predicate`` attribute was added. - - Examples - -------- - - Creating a basic check to see if the command invoker is you. - - .. code-block:: python3 - - def check_if_it_is_me(ctx): - return ctx.message.author.id == 85309593344815104 - - - @bot.command() - @commands.check(check_if_it_is_me) - async def only_for_me(ctx): - await ctx.send("I know you!") - - Transforming common checks into its own decorator: - - .. code-block:: python3 - - def is_me(): - def predicate(ctx): - return ctx.message.author.id == 85309593344815104 - - return commands.check(predicate) - - - @bot.command() - @is_me() - async def only_me(ctx): - await ctx.send("Only you!") - - Parameters - ---------- - predicate: Callable[[:class:`Context`], :class:`bool`] - The predicate to check if the command should be invoked. - """ - - def decorator(func: Command | CoroFunc) -> Command | CoroFunc: - if isinstance(func, _BaseCommand): - func.checks.append(predicate) - else: - if not hasattr(func, "__commands_checks__"): - func.__commands_checks__ = [] - - func.__commands_checks__.append(predicate) - - return func - - if inspect.iscoroutinefunction(predicate): - decorator.predicate = predicate - else: - - @functools.wraps(predicate) - async def wrapper(ctx): - return predicate(ctx) # type: ignore - - decorator.predicate = wrapper - - return decorator # type: ignore - - -def check_any(*checks: Check) -> Callable[[T], T]: - r"""A :func:`check` that is added that checks if any of the checks passed - will pass, i.e. using logical OR. - - If all checks fail then :exc:`.CheckAnyFailure` is raised to signal the failure. - It inherits from :exc:`.CheckFailure`. - - .. note:: - - The ``predicate`` attribute for this function **is** a coroutine. - - .. versionadded:: 1.3 - - Examples - -------- - - Creating a basic check to see if it's the bot owner or - the server owner: - - .. code-block:: python3 - - def is_guild_owner(): - def predicate(ctx): - return ctx.guild is not None and ctx.guild.owner_id == ctx.author.id - - return commands.check(predicate) - - - @bot.command() - @commands.check_any(commands.is_owner(), is_guild_owner()) - async def only_for_owners(ctx): - await ctx.send("Hello mister owner!") - - Parameters - ---------- - \*checks: Callable[[:class:`Context`], :class:`bool`] - An argument list of checks that have been decorated with - the :func:`check` decorator. - - Raises - ------ - TypeError - A check passed has not been decorated with the :func:`check` - decorator. - - """ - - unwrapped = [] - for wrapped in checks: - try: - pred = wrapped.predicate - except AttributeError: - raise TypeError(f"{wrapped!r} must be wrapped by commands.check decorator") from None - else: - unwrapped.append(pred) - - async def predicate(ctx: Context) -> bool: - errors = [] - for func in unwrapped: - try: - value = await func(ctx) - except CheckFailure as e: - errors.append(e) - else: - if value: - return True - # if we're here, all checks failed - raise CheckAnyFailure(unwrapped, errors) - - return check(predicate) - - -def has_role(item: int | str) -> Callable[[T], T]: - """A :func:`.check` that is added that checks if the member invoking the - command has the role specified via the name or ID specified. - - If a string is specified, you must give the exact name of the role, including - caps and spelling. - - If an integer is specified, you must give the exact snowflake ID of the role. - - If the message is invoked in a private message context then the check will - return ``False``. - - This check raises one of two special exceptions, :exc:`.MissingRole` if the user - is missing a role, or :exc:`.NoPrivateMessage` if it is used in a private message. - Both inherit from :exc:`.CheckFailure`. - - .. versionchanged:: 1.1 - - Raise :exc:`.MissingRole` or :exc:`.NoPrivateMessage` - instead of generic :exc:`.CheckFailure` - - Parameters - ---------- - item: Union[:class:`int`, :class:`str`] - The name or ID of the role to check. - """ - - def predicate(ctx: Context) -> bool: - if ctx.guild is None: - raise NoPrivateMessage() - - # ctx.guild is None doesn't narrow ctx.author to Member - if isinstance(item, int): - role = discord.utils.find(lambda r: r.id == item, ctx.author.roles) # type: ignore - else: - role = discord.utils.find(lambda r: r.name == item, ctx.author.roles) # type: ignore - if role is None: - raise MissingRole(item) - return True - - return check(predicate) - - -def has_any_role(*items: int | str) -> Callable[[T], T]: - r"""A :func:`.check` that is added that checks if the member invoking the - command has **any** of the roles specified. This means that if they have - one out of the three roles specified, then this check will return `True`. - - Similar to :func:`.has_role`\, the names or IDs passed in must be exact. - - This check raises one of two special exceptions, :exc:`.MissingAnyRole` if the user - is missing all roles, or :exc:`.NoPrivateMessage` if it is used in a private message. - Both inherit from :exc:`.CheckFailure`. - - .. versionchanged:: 1.1 - - Raise :exc:`.MissingAnyRole` or :exc:`.NoPrivateMessage` - instead of generic :exc:`.CheckFailure` - - Parameters - ---------- - items: List[Union[:class:`str`, :class:`int`]] - An argument list of names or IDs to check that the member has roles wise. - - Example - ------- - - .. code-block:: python3 - - @bot.command() - @commands.has_any_role("Library Devs", "Moderators", 492212595072434186) - async def cool(ctx): - await ctx.send("You are cool indeed") - """ - - def predicate(ctx): - if ctx.guild is None: - raise NoPrivateMessage() - - # ctx.guild is None doesn't narrow ctx.author to Member - for item in items: - if isinstance(item, int): - if any(role.id == item for role in ctx.author.roles): - return True - else: - if any(role.name == item for role in ctx.author.roles): - return True - raise MissingAnyRole(list(items)) - - return check(predicate) - - -def bot_has_role(item: int) -> Callable[[T], T]: - """Similar to :func:`.has_role` except checks if the bot itself has the - role. - - This check raises one of two special exceptions, :exc:`.BotMissingRole` if the bot - is missing the role, or :exc:`.NoPrivateMessage` if it is used in a private message. - Both inherit from :exc:`.CheckFailure`. - - .. versionchanged:: 1.1 - - Raise :exc:`.BotMissingRole` or :exc:`.NoPrivateMessage` - instead of generic :exc:`.CheckFailure` - """ - - def predicate(ctx): - if ctx.guild is None: - raise NoPrivateMessage() - - me = ctx.me - if isinstance(item, int): - role = discord.utils.find(lambda r: r.id == item, me.roles) - else: - role = discord.utils.find(lambda r: r.name == item, me.roles) - if role is None: - raise BotMissingRole(item) - return True - - return check(predicate) - - -def bot_has_any_role(*items: int) -> Callable[[T], T]: - """Similar to :func:`.has_any_role` except checks if the bot itself has - any of the roles listed. - - This check raises one of two special exceptions, :exc:`.BotMissingAnyRole` if the bot - is missing all roles, or :exc:`.NoPrivateMessage` if it is used in a private message. - Both inherit from :exc:`.CheckFailure`. - - .. versionchanged:: 1.1 - - Raise :exc:`.BotMissingAnyRole` or :exc:`.NoPrivateMessage` - instead of generic :exc:`.CheckFailure`. - """ - - def predicate(ctx): - if ctx.guild is None: - raise NoPrivateMessage() - - me = ctx.me - for item in items: - if isinstance(item, int): - if any(role.id == item for role in me.roles): - return True - else: - if any(role.name == item for role in me.roles): - return True - - raise BotMissingAnyRole(list(items)) - - return check(predicate) - - -def has_permissions(**perms: bool) -> Callable[[T], T]: - r"""A :func:`.check` that is added that checks if the member has all of - the permissions necessary. - - Note that this check operates on the current channel permissions, not the - guild wide permissions. - - The permissions passed in must be exactly like the properties shown under - :class:`.discord.Permissions`. - - This check raises a special exception, :exc:`.MissingPermissions` - that is inherited from :exc:`.CheckFailure`. - - If the command is executed within a DM, it returns ``True``. - - Parameters - ---------- - \*\*perms: Dict[:class:`str`, :class:`bool`] - An argument list of permissions to check for. - - Example - ------- - - .. code-block:: python3 - - @bot.command() - @commands.has_permissions(manage_messages=True) - async def test(ctx): - await ctx.send("You can manage messages.") - - """ - - invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) - if invalid: - raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") - - def predicate(ctx: Context) -> bool: - if ctx.channel.type == ChannelType.private: - return True - permissions = ctx.channel.permissions_for(ctx.author) # type: ignore - - missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] - - if not missing: - return True - - raise MissingPermissions(missing) - - return check(predicate) - - -def bot_has_permissions(**perms: bool) -> Callable[[T], T]: - """Similar to :func:`.has_permissions` except checks if the bot itself has - the permissions listed. - - This check raises a special exception, :exc:`.BotMissingPermissions` - that is inherited from :exc:`.CheckFailure`. - """ - - invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) - if invalid: - raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") - - def predicate(ctx: Context) -> bool: - guild = ctx.guild - me = guild.me if guild is not None else ctx.bot.user - if ctx.channel.type == ChannelType.private: - return True - - if hasattr(ctx, "app_permissions"): - permissions = ctx.app_permissions - else: - permissions = ctx.channel.permissions_for(me) # type: ignore - - missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] - - if not missing: - return True - - raise BotMissingPermissions(missing) - - return check(predicate) - - -def has_guild_permissions(**perms: bool) -> Callable[[T], T]: - """Similar to :func:`.has_permissions`, but operates on guild wide - permissions instead of the current channel permissions. - - If this check is called in a DM context, it will raise an - exception, :exc:`.NoPrivateMessage`. - - .. versionadded:: 1.3 - """ - - invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) - if invalid: - raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") - - def predicate(ctx: Context) -> bool: - if not ctx.guild: - raise NoPrivateMessage - - permissions = ctx.author.guild_permissions # type: ignore - missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] - - if not missing: - return True - - raise MissingPermissions(missing) - - return check(predicate) - - -def bot_has_guild_permissions(**perms: bool) -> Callable[[T], T]: - """Similar to :func:`.has_guild_permissions`, but checks the bot - members guild permissions. - - .. versionadded:: 1.3 - """ - - invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) - if invalid: - raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") - - def predicate(ctx: Context) -> bool: - if not ctx.guild: - raise NoPrivateMessage - - permissions = ctx.me.guild_permissions # type: ignore - missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] - - if not missing: - return True - - raise BotMissingPermissions(missing) - - return check(predicate) - - -def dm_only() -> Callable[[T], T]: - """A :func:`.check` that indicates this command must only be used in a - DM context. Only private messages are allowed when - using the command. - - This check raises a special exception, :exc:`.PrivateMessageOnly` - that is inherited from :exc:`.CheckFailure`. - - .. versionadded:: 1.1 - """ - - def predicate(ctx: Context) -> bool: - if ctx.guild is not None: - raise PrivateMessageOnly() - return True - - return check(predicate) - - -def guild_only() -> Callable[[T], T]: - """A :func:`.check` that indicates this command must only be used in a - guild context only. Basically, no private messages are allowed when - using the command. - - This check raises a special exception, :exc:`.NoPrivateMessage` - that is inherited from :exc:`.CheckFailure`. - """ - - def predicate(ctx: Context) -> bool: - if ctx.guild is None: - raise NoPrivateMessage() - return True - - return check(predicate) - - -def is_owner() -> Callable[[T], T]: - """A :func:`.check` that checks if the person invoking this command is the - owner of the bot. - - This is powered by :meth:`.Bot.is_owner`. - - This check raises a special exception, :exc:`.NotOwner` that is derived - from :exc:`.CheckFailure`. - """ - - async def predicate(ctx: Context) -> bool: - if not await ctx.bot.is_owner(ctx.author): - raise NotOwner("You do not own this bot.") - return True - - return check(predicate) - - -def is_nsfw() -> Callable[[T], T]: - """A :func:`.check` that checks if the channel is a NSFW channel. - - This check raises a special exception, :exc:`.NSFWChannelRequired` - that is derived from :exc:`.CheckFailure`. - - .. versionchanged:: 1.1 - - Raise :exc:`.NSFWChannelRequired` instead of generic :exc:`.CheckFailure`. - DM channels will also now pass this check. - """ - - def pred(ctx: Context) -> bool: - ch = ctx.channel - if ctx.guild is None or (isinstance(ch, (discord.TextChannel, discord.Thread)) and ch.is_nsfw()): - return True - raise NSFWChannelRequired(ch) # type: ignore - - return check(pred) - - -def cooldown( - rate: int, - per: float, - type: BucketType | Callable[[Message], Any] = BucketType.default, -) -> Callable[[T], T]: - """A decorator that adds a cooldown to a command - - A cooldown allows a command to only be used a specific amount - of times in a specific time frame. These cooldowns can be based - either on a per-guild, per-channel, per-user, per-role or global basis. - Denoted by the third argument of ``type`` which must be of enum - type :class:`.BucketType`. - - If a cooldown is triggered, then :exc:`.CommandOnCooldown` is triggered in - :func:`.on_command_error` and the local error handler. - - A command can only have a single cooldown. - - Parameters - ---------- - rate: :class:`int` - The number of times a command can be used before triggering a cooldown. - per: :class:`float` - The amount of seconds to wait for a cooldown when it's been triggered. - type: Union[:class:`.BucketType`, Callable[[:class:`.Message`], Any]] - The type of cooldown to have. If callable, should return a key for the mapping. - - .. versionchanged:: 1.7 - Callables are now supported for custom bucket types. - """ - - def decorator(func: Command | CoroFunc) -> Command | CoroFunc: - if isinstance(func, (Command, ApplicationCommand)): - func._buckets = CooldownMapping(Cooldown(rate, per), type) - else: - func.__commands_cooldown__ = CooldownMapping(Cooldown(rate, per), type) - return func - - return decorator # type: ignore - - -def dynamic_cooldown( - cooldown: BucketType | Callable[[Message], Any], - type: BucketType = BucketType.default, -) -> Callable[[T], T]: - """A decorator that adds a dynamic cooldown to a command - - This differs from :func:`.cooldown` in that it takes a function that - accepts a single parameter of type :class:`.discord.Message` and must - return a :class:`.Cooldown` or ``None``. If ``None`` is returned then - that cooldown is effectively bypassed. - - A cooldown allows a command to only be used a specific amount - of times in a specific time frame. These cooldowns can be based - either on a per-guild, per-channel, per-user, per-role or global basis. - Denoted by the third argument of ``type`` which must be of enum - type :class:`.BucketType`. - - If a cooldown is triggered, then :exc:`.CommandOnCooldown` is triggered in - :func:`.on_command_error` and the local error handler. - - A command can only have a single cooldown. - - .. versionadded:: 2.0 - - Parameters - ---------- - cooldown: Callable[[:class:`.discord.Message`], Optional[:class:`.Cooldown`]] - A function that takes a message and returns a cooldown that will - apply to this invocation or ``None`` if the cooldown should be bypassed. - type: :class:`.BucketType` - The type of cooldown to have. - """ - if not callable(cooldown): - raise TypeError("A callable must be provided") - - def decorator(func: Command | CoroFunc) -> Command | CoroFunc: - if isinstance(func, Command): - func._buckets = DynamicCooldownMapping(cooldown, type) - else: - func.__commands_cooldown__ = DynamicCooldownMapping(cooldown, type) - return func - - return decorator # type: ignore - - -def max_concurrency(number: int, per: BucketType = BucketType.default, *, wait: bool = False) -> Callable[[T], T]: - """A decorator that adds a maximum concurrency to a command - - This enables you to only allow a certain number of command invocations at the same time, - for example if a command takes too long or if only one user can use it at a time. This - differs from a cooldown in that there is no set waiting period or token bucket -- only - a set number of people can run the command. - - .. versionadded:: 1.3 - - Parameters - ---------- - number: :class:`int` - The maximum number of invocations of this command that can be running at the same time. - per: :class:`.BucketType` - The bucket that this concurrency is based on, e.g. ``BucketType.guild`` would allow - it to be used up to ``number`` times per guild. - wait: :class:`bool` - Whether the command should wait for the queue to be over. If this is set to ``False`` - then instead of waiting until the command can run again, the command raises - :exc:`.MaxConcurrencyReached` to its error handler. If this is set to ``True`` - then the command waits until it can be executed. - """ - - def decorator(func: Command | CoroFunc) -> Command | CoroFunc: - value = MaxConcurrency(number, per=per, wait=wait) - if isinstance(func, (Command, ApplicationCommand)): - func._max_concurrency = value - else: - func.__commands_max_concurrency__ = value - return func - - return decorator # type: ignore - - -def before_invoke(coro) -> Callable[[T], T]: - """A decorator that registers a coroutine as a pre-invoke hook. - - This allows you to refer to one before invoke hook for several commands that - do not have to be within the same cog. - - .. versionadded:: 1.4 - - Example - ------- - - .. code-block:: python3 - - async def record_usage(ctx): - print(ctx.author, "used", ctx.command, "at", ctx.message.created_at) - - - @bot.command() - @commands.before_invoke(record_usage) - async def who(ctx): # Output: used who at