mirror of
				https://github.com/Rapptz/discord.py.git
				synced 2025-10-31 05:23:03 +00:00 
			
		
		
		
	Add support for app command checks
This does not include any built-in checks due to design considerations.
This commit is contained in:
		| @@ -49,11 +49,11 @@ import re | ||||
| from ..enums import AppCommandOptionType, AppCommandType | ||||
| from .models import Choice | ||||
| from .transformers import annotation_to_parameter, CommandParameter, NoneType | ||||
| from .errors import AppCommandError, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered | ||||
| from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered | ||||
| from ..message import Message | ||||
| from ..user import User | ||||
| from ..member import Member | ||||
| from ..utils import resolve_annotation, MISSING, is_inside_class | ||||
| from ..utils import resolve_annotation, MISSING, is_inside_class, maybe_coroutine, async_all | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from typing_extensions import ParamSpec, Concatenate | ||||
| @@ -74,6 +74,7 @@ __all__ = ( | ||||
|     'context_menu', | ||||
|     'command', | ||||
|     'describe', | ||||
|     'check', | ||||
|     'choices', | ||||
|     'autocomplete', | ||||
|     'guilds', | ||||
| @@ -91,6 +92,7 @@ Error = Union[ | ||||
|     Callable[[GroupT, 'Interaction', AppCommandError], Coro[Any]], | ||||
|     Callable[['Interaction', AppCommandError], Coro[Any]], | ||||
| ] | ||||
| Check = Callable[['Interaction'], Union[bool, Coro[bool]]] | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| @@ -121,6 +123,7 @@ else: | ||||
|     AutocompleteCallback = Callable[..., Coro[T]] | ||||
|  | ||||
|  | ||||
| CheckInputParameter = Union['Command[Any, ..., Any]', 'ContextMenu', CommandCallback, ContextMenuCallback] | ||||
| VALID_SLASH_COMMAND_NAME = re.compile(r'^[\w-]{1,32}$') | ||||
| VALID_CONTEXT_MENU_NAME = re.compile(r'^[\w\s-]{1,32}$') | ||||
| CAMEL_CASE_REGEX = re.compile(r'(?<!^)(?=[A-Z])') | ||||
| @@ -356,6 +359,12 @@ class Command(Generic[GroupT, P, T]): | ||||
|     description: :class:`str` | ||||
|         The description of the application command. This shows up in the UI to describe | ||||
|         the application command. | ||||
|     checks | ||||
|         A list of predicates that take a :class:`~discord.Interaction` parameter | ||||
|         to indicate whether the command callback should be executed. If an exception | ||||
|         is necessary to be thrown to signal failure, then one inherited from | ||||
|         :exc:`AppCommandError` should be used. If all the checks fail without | ||||
|         propagating an exception, :exc:`CheckFailure` is raised. | ||||
|     parent: Optional[:class:`Group`] | ||||
|         The parent application command. ``None`` if there isn't one. | ||||
|     """ | ||||
| @@ -386,6 +395,7 @@ class Command(Generic[GroupT, P, T]): | ||||
|             pass | ||||
|  | ||||
|         self._params: Dict[str, CommandParameter] = _extract_parameters_from_callback(callback, callback.__globals__) | ||||
|         self.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', []) | ||||
|         self._guild_ids: Optional[List[int]] = guild_ids or getattr( | ||||
|             callback, '__discord_app_commands_default_guilds__', None | ||||
|         ) | ||||
| @@ -406,6 +416,7 @@ class Command(Generic[GroupT, P, T]): | ||||
|         copy = cls.__new__(cls) | ||||
|         copy.name = self.name | ||||
|         copy._guild_ids = self._guild_ids | ||||
|         copy.checks = self.checks | ||||
|         copy.description = self.description | ||||
|         copy._attr = self._attr | ||||
|         copy._callback = self._callback | ||||
| @@ -443,6 +454,9 @@ class Command(Generic[GroupT, P, T]): | ||||
|                 await parent.parent.on_error(interaction, self, error) | ||||
|  | ||||
|     async def _invoke_with_namespace(self, interaction: Interaction, namespace: Namespace) -> T: | ||||
|         if not await self._check_can_run(interaction): | ||||
|             raise CheckFailure(f'The check functions for command {self.name!r} failed.') | ||||
|  | ||||
|         values = namespace.__dict__ | ||||
|         for name, param in self._params.items(): | ||||
|             try: | ||||
| @@ -515,6 +529,34 @@ class Command(Generic[GroupT, P, T]): | ||||
|         parent = self.parent | ||||
|         return parent.parent or parent | ||||
|  | ||||
|     async def _check_can_run(self, interaction: Interaction) -> bool: | ||||
|         if self.parent is not None and self.parent is not self.binding: | ||||
|             # For commands with a parent which isn't the binding, i.e. | ||||
|             # <binding> | ||||
|             #     <parent> | ||||
|             #         <command> | ||||
|             # The parent check needs to be called first | ||||
|             if not await maybe_coroutine(self.parent.interaction_check, interaction): | ||||
|                 return False | ||||
|  | ||||
|         if self.binding is not None: | ||||
|             try: | ||||
|                 # Type checker does not like runtime attribute retrieval | ||||
|                 check: Check = self.binding.interaction_check  # type: ignore | ||||
|             except AttributeError: | ||||
|                 pass | ||||
|             else: | ||||
|                 ret = await maybe_coroutine(check, interaction) | ||||
|                 if not ret: | ||||
|                     return False | ||||
|  | ||||
|         predicates = self.checks | ||||
|         if not predicates: | ||||
|             return True | ||||
|  | ||||
|         # Type checker does not understand negative narrowing cases like this function | ||||
|         return await async_all(f(interaction) for f in predicates)  # type: ignore | ||||
|  | ||||
|     def error(self, coro: Error[GroupT]) -> Error[GroupT]: | ||||
|         """A decorator that registers a coroutine as a local error handler. | ||||
|  | ||||
| @@ -611,6 +653,36 @@ class Command(Generic[GroupT, P, T]): | ||||
|  | ||||
|         return decorator | ||||
|  | ||||
|     def add_check(self, func: Check, /) -> None: | ||||
|         """Adds a check to the command. | ||||
|  | ||||
|         This is the non-decorator interface to :func:`check`. | ||||
|  | ||||
|         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. | ||||
|  | ||||
|         Parameters | ||||
|         ----------- | ||||
|         func | ||||
|             The function to remove from the checks. | ||||
|         """ | ||||
|  | ||||
|         try: | ||||
|             self.checks.remove(func) | ||||
|         except ValueError: | ||||
|             pass | ||||
|  | ||||
|  | ||||
| class ContextMenu: | ||||
|     """A class that implements a context menu application command. | ||||
| @@ -629,6 +701,12 @@ class ContextMenu: | ||||
|         The name of the context menu. | ||||
|     type: :class:`.AppCommandType` | ||||
|         The type of context menu application command. | ||||
|     checks | ||||
|         A list of predicates that take a :class:`~discord.Interaction` parameter | ||||
|         to indicate whether the command callback should be executed. If an exception | ||||
|         is necessary to be thrown to signal failure, then one inherited from | ||||
|         :exc:`AppCommandError` should be used. If all the checks fail without | ||||
|         propagating an exception, :exc:`CheckFailure` is raised. | ||||
|     """ | ||||
|  | ||||
|     def __init__( | ||||
| @@ -649,6 +727,7 @@ class ContextMenu: | ||||
|         self._annotation = annotation | ||||
|         self.module: Optional[str] = callback.__module__ | ||||
|         self._guild_ids = guild_ids | ||||
|         self.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', []) | ||||
|  | ||||
|     @property | ||||
|     def callback(self) -> ContextMenuCallback: | ||||
| @@ -667,6 +746,7 @@ class ContextMenu: | ||||
|         self._annotation = annotation | ||||
|         self.module = callback.__module__ | ||||
|         self._guild_ids = None | ||||
|         self.checks = getattr(callback, '__discord_app_commands_checks__', []) | ||||
|         return self | ||||
|  | ||||
|     def to_dict(self) -> Dict[str, Any]: | ||||
| @@ -675,14 +755,55 @@ class ContextMenu: | ||||
|             'type': self.type.value, | ||||
|         } | ||||
|  | ||||
|     async def _check_can_run(self, interaction: Interaction) -> bool: | ||||
|         predicates = self.checks | ||||
|         if not predicates: | ||||
|             return True | ||||
|  | ||||
|         # Type checker does not understand negative narrowing cases like this function | ||||
|         return await async_all(f(interaction) for f in predicates)  # type: ignore | ||||
|  | ||||
|     async def _invoke(self, interaction: Interaction, arg: Any): | ||||
|         try: | ||||
|             if not await self._check_can_run(interaction): | ||||
|                 raise CheckFailure(f'The check functions for context menu {self.name!r} failed.') | ||||
|  | ||||
|             await self._callback(interaction, arg) | ||||
|         except AppCommandError: | ||||
|             raise | ||||
|         except Exception as e: | ||||
|             raise CommandInvokeError(self, e) from e | ||||
|  | ||||
|     def add_check(self, func: Check, /) -> None: | ||||
|         """Adds a check to the command. | ||||
|  | ||||
|         This is the non-decorator interface to :func:`check`. | ||||
|  | ||||
|         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. | ||||
|  | ||||
|         Parameters | ||||
|         ----------- | ||||
|         func | ||||
|             The function to remove from the checks. | ||||
|         """ | ||||
|  | ||||
|         try: | ||||
|             self.checks.remove(func) | ||||
|         except ValueError: | ||||
|             pass | ||||
|  | ||||
|  | ||||
| class Group: | ||||
|     """A class that implements an application command group. | ||||
| @@ -857,6 +978,37 @@ class Group: | ||||
|  | ||||
|         pass | ||||
|  | ||||
|     async def interaction_check(self, interaction: Interaction) -> bool: | ||||
|         """|coro| | ||||
|  | ||||
|         A callback that is called when an interaction happens within the group | ||||
|         that checks whether a command inside the group should be executed. | ||||
|  | ||||
|         This is useful to override if, for example, you want to ensure that the | ||||
|         interaction author is a given user. | ||||
|  | ||||
|         The default implementation of this returns ``True``. | ||||
|  | ||||
|         .. note:: | ||||
|  | ||||
|             If an exception occurs within the body then the check | ||||
|             is considered a failure and error handlers such as | ||||
|             :meth:`on_error` is called. See :exc:`AppCommandError` | ||||
|             for more information. | ||||
|  | ||||
|         Parameters | ||||
|         ----------- | ||||
|         interaction: :class:`~discord.Interaction` | ||||
|             The interaction that occurred. | ||||
|  | ||||
|         Returns | ||||
|         --------- | ||||
|         :class:`bool` | ||||
|             Whether the view children's callbacks should be called. | ||||
|         """ | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def add_command(self, command: Union[Command[Any, ..., Any], Group], /, *, override: bool = False) -> None: | ||||
|         """Adds a command or group to this group's internal list of commands. | ||||
|  | ||||
| @@ -1260,3 +1412,62 @@ def guilds(*guild_ids: Union[Snowflake, int]) -> Callable[[T], T]: | ||||
|         return inner | ||||
|  | ||||
|     return decorator | ||||
|  | ||||
|  | ||||
| def check(predicate: Check) -> Callable[[T], T]: | ||||
|     r"""A decorator that adds a check to an application command. | ||||
|  | ||||
|     These checks should be predicates that take in a single parameter taking | ||||
|     a :class:`~discord.Interaction`. If the check returns a ``False``\-like value then | ||||
|     during invocation a :exc:`CheckFailure` exception is raised and sent to | ||||
|     the appropriate error handlers. | ||||
|  | ||||
|     These checks can be either a coroutine or not. | ||||
|  | ||||
|     Examples | ||||
|     --------- | ||||
|  | ||||
|     Creating a basic check to see if the command invoker is you. | ||||
|  | ||||
|     .. code-block:: python3 | ||||
|  | ||||
|         def check_if_it_is_me(interaction: discord.Interaction) -> bool: | ||||
|             return interaction.user.id == 85309593344815104 | ||||
|  | ||||
|         @tree.command() | ||||
|         @app_commands.check(check_if_it_is_me) | ||||
|         async def only_for_me(interaction: discord.Interaction): | ||||
|             await interaction.response.send_message('I know you!', ephemeral=True) | ||||
|  | ||||
|     Transforming common checks into its own decorator: | ||||
|  | ||||
|     .. code-block:: python3 | ||||
|  | ||||
|         def is_me(): | ||||
|             def predicate(interaction: discord.Interaction) -> bool: | ||||
|                 return interaction.user.id == 85309593344815104 | ||||
|             return commands.check(predicate) | ||||
|  | ||||
|         @tree.command() | ||||
|         @is_me() | ||||
|         async def only_me(interaction: discord.Interaction): | ||||
|             await interaction.response.send_message('Only you!') | ||||
|  | ||||
|     Parameters | ||||
|     ----------- | ||||
|     predicate: Callable[[:class:`~discord.Interaction`], :class:`bool`] | ||||
|         The predicate to check if the command should be invoked. | ||||
|     """ | ||||
|  | ||||
|     def decorator(func: CheckInputParameter) -> CheckInputParameter: | ||||
|         if isinstance(func, (Command, ContextMenu)): | ||||
|             func.checks.append(predicate) | ||||
|         else: | ||||
|             if not hasattr(func, '__discord_app_commands_checks__'): | ||||
|                 func.__discord_app_commands_checks__ = [] | ||||
|  | ||||
|             func.__discord_app_commands_checks__.append(predicate) | ||||
|  | ||||
|         return func | ||||
|  | ||||
|     return decorator  # type: ignore | ||||
|   | ||||
| @@ -34,6 +34,7 @@ __all__ = ( | ||||
|     'AppCommandError', | ||||
|     'CommandInvokeError', | ||||
|     'TransformerError', | ||||
|     'CheckFailure', | ||||
|     'CommandAlreadyRegistered', | ||||
|     'CommandSignatureMismatch', | ||||
|     'CommandNotFound', | ||||
| @@ -128,6 +129,17 @@ class TransformerError(AppCommandError): | ||||
|         super().__init__(f'Failed to convert {value} to {result_type!s}') | ||||
|  | ||||
|  | ||||
| class CheckFailure(AppCommandError): | ||||
|     """An exception raised when check predicates in a command have failed. | ||||
|  | ||||
|     This inherits from :exc:`~discord.app_commands.AppCommandError`. | ||||
|  | ||||
|     .. versionadded:: 2.0 | ||||
|     """ | ||||
|  | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class CommandAlreadyRegistered(AppCommandError): | ||||
|     """An exception raised when a command is already registered. | ||||
|  | ||||
|   | ||||
| @@ -465,6 +465,9 @@ Decorators | ||||
| .. autofunction:: discord.app_commands.choices | ||||
|     :decorator: | ||||
|  | ||||
| .. autofunction:: discord.app_commands.check | ||||
|     :decorator: | ||||
|  | ||||
| .. autofunction:: discord.app_commands.autocomplete | ||||
|     :decorator: | ||||
|  | ||||
| @@ -518,6 +521,9 @@ Exceptions | ||||
| .. autoexception:: discord.app_commands.TransformerError | ||||
|     :members: | ||||
|  | ||||
| .. autoexception:: discord.app_commands.CheckFailure | ||||
|     :members: | ||||
|  | ||||
| .. autoexception:: discord.app_commands.CommandAlreadyRegistered | ||||
|     :members: | ||||
|  | ||||
| @@ -536,6 +542,7 @@ Exception Hierarchy | ||||
|         - :exc:`~discord.app_commands.AppCommandError` | ||||
|             - :exc:`~discord.app_commands.CommandInvokeError` | ||||
|             - :exc:`~discord.app_commands.TransformerError` | ||||
|             - :exc:`~discord.app_commands.CheckFailure` | ||||
|             - :exc:`~discord.app_commands.CommandAlreadyRegistered` | ||||
|             - :exc:`~discord.app_commands.CommandSignatureMismatch` | ||||
|             - :exc:`~discord.app_commands.CommandNotFound` | ||||
|   | ||||
		Reference in New Issue
	
	Block a user