You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2886 lines
107 KiB

"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
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 typing import (
Any,
Callable,
ClassVar,
Coroutine,
Dict,
Generator,
Generic,
List,
MutableMapping,
Optional,
Set,
TYPE_CHECKING,
Tuple,
Type,
TypeVar,
Union,
overload,
)
import re
from copy import copy as shallow_copy
from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale
from .installs import AppCommandContext, AppInstallationType
from .models import Choice
from .transformers import annotation_to_parameter, CommandParameter, NoneType
from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered
from .translator import TranslationContextLocation, TranslationContext, Translator, locale_str
from ..message import Message
from ..user import User
from ..member import Member
from ..permissions import Permissions
from ..utils import resolve_annotation, MISSING, is_inside_class, maybe_coroutine, async_all, _shorten, _to_kebab_case
if TYPE_CHECKING:
from typing_extensions import ParamSpec, Concatenate
from ..interactions import Interaction
from ..abc import Snowflake
from .namespace import Namespace
from .models import ChoiceT
from .tree import CommandTree
from .._types import ClientT
# Generally, these two libraries are supposed to be separate from each other.
# However, for type hinting purposes it's unfortunately necessary for one to
# reference the other to prevent type checking errors in callbacks
from discord.ext import commands
ErrorFunc = Callable[[Interaction, AppCommandError], Coroutine[Any, Any, None]]
__all__ = (
'Command',
'ContextMenu',
'Group',
'Parameter',
'context_menu',
'command',
'describe',
'check',
'rename',
'choices',
'autocomplete',
'guilds',
'guild_only',
'dm_only',
'private_channel_only',
'allowed_contexts',
'guild_install',
'user_install',
'allowed_installs',
'default_permissions',
)
if TYPE_CHECKING:
P = ParamSpec('P')
else:
P = TypeVar('P')
T = TypeVar('T')
F = TypeVar('F', bound=Callable[..., Any])
GroupT = TypeVar('GroupT', bound='Binding')
Coro = Coroutine[Any, Any, T]
UnboundError = Callable[['Interaction[Any]', AppCommandError], Coro[Any]]
Error = Union[
Callable[[GroupT, 'Interaction[Any]', AppCommandError], Coro[Any]],
UnboundError,
]
Check = Callable[['Interaction[Any]'], Union[bool, Coro[bool]]]
Binding = Union['Group', 'commands.Cog']
if TYPE_CHECKING:
CommandCallback = Union[
Callable[Concatenate[GroupT, 'Interaction[Any]', P], Coro[T]],
Callable[Concatenate['Interaction[Any]', P], Coro[T]],
]
ContextMenuCallback = Union[
# If groups end up support context menus these would be uncommented
# Callable[[GroupT, 'Interaction', Member], Coro[Any]],
# Callable[[GroupT, 'Interaction', User], Coro[Any]],
# Callable[[GroupT, 'Interaction', Message], Coro[Any]],
# Callable[[GroupT, 'Interaction', Union[Member, User]], Coro[Any]],
Callable[['Interaction[Any]', Member], Coro[Any]],
Callable[['Interaction[Any]', User], Coro[Any]],
Callable[['Interaction[Any]', Message], Coro[Any]],
Callable[['Interaction[Any]', Union[Member, User]], Coro[Any]],
]
AutocompleteCallback = Union[
Callable[[GroupT, 'Interaction[Any]', str], Coro[List[Choice[ChoiceT]]]],
Callable[['Interaction[Any]', str], Coro[List[Choice[ChoiceT]]]],
]
else:
CommandCallback = Callable[..., Coro[T]]
ContextMenuCallback = Callable[..., Coro[T]]
AutocompleteCallback = Callable[..., Coro[T]]
CheckInputParameter = Union['Command[Any, ..., Any]', 'ContextMenu', 'CommandCallback[Any, ..., Any]', ContextMenuCallback]
# The re module doesn't support \p{} so we have to list characters from Thai and Devanagari manually.
THAI_COMBINING = r'\u0e31-\u0e3a\u0e47-\u0e4e'
DEVANAGARI_COMBINING = r'\u0900-\u0903\u093a\u093b\u093c\u093e\u093f\u0940-\u094f\u0955\u0956\u0957\u0962\u0963'
VALID_SLASH_COMMAND_NAME = re.compile(r'^[-_\w' + THAI_COMBINING + DEVANAGARI_COMBINING + r']{1,32}$')
ARG_NAME_SUBREGEX = r'(?:\\?\*){0,2}(?P<name>\w+)'
ARG_DESCRIPTION_SUBREGEX = r'(?P<description>(?:.|\n)+?(?:\Z|\r?\n(?=[\S\r\n])))'
ARG_TYPE_SUBREGEX = r'(?:.+)'
GOOGLE_DOCSTRING_ARG_REGEX = re.compile(
rf'^{ARG_NAME_SUBREGEX}[ \t]*(?:\({ARG_TYPE_SUBREGEX}\))?[ \t]*:[ \t]*{ARG_DESCRIPTION_SUBREGEX}',
re.MULTILINE,
)
SPHINX_DOCSTRING_ARG_REGEX = re.compile(
rf'^:param {ARG_NAME_SUBREGEX}:[ \t]+{ARG_DESCRIPTION_SUBREGEX}',
re.MULTILINE,
)
NUMPY_DOCSTRING_ARG_REGEX = re.compile(
rf'^{ARG_NAME_SUBREGEX}(?:[ \t]*:)?(?:[ \t]+{ARG_TYPE_SUBREGEX})?[ \t]*\r?\n[ \t]+{ARG_DESCRIPTION_SUBREGEX}',
re.MULTILINE,
)
def _parse_args_from_docstring(func: Callable[..., Any], params: Dict[str, CommandParameter]) -> Dict[str, str]:
docstring = inspect.getdoc(func)
if docstring is None:
return {}
# Extract the arguments
# Note: These are loose regexes, but they are good enough for our purposes
# For Google-style, look only at the lines that are indented
section_lines = inspect.cleandoc('\n'.join(line for line in docstring.splitlines() if line.startswith(' ')))
docstring_styles = (
GOOGLE_DOCSTRING_ARG_REGEX.finditer(section_lines),
SPHINX_DOCSTRING_ARG_REGEX.finditer(docstring),
NUMPY_DOCSTRING_ARG_REGEX.finditer(docstring),
)
return {
m.group('name'): m.group('description') for matches in docstring_styles for m in matches if m.group('name') in params
}
def validate_name(name: str) -> str:
match = VALID_SLASH_COMMAND_NAME.match(name)
if match is None:
raise ValueError(
f'{name!r} must be between 1-32 characters and contain only lower-case letters, numbers, hyphens, or underscores.'
)
# Ideally, name.islower() would work instead but since certain characters
# are Lo (e.g. CJK) those don't pass the test. I'd use `casefold` instead as
# well, but chances are the server-side check is probably something similar to
# this code anyway.
if name.lower() != name:
raise ValueError(f'{name!r} must be all lower-case')
return name
def validate_context_menu_name(name: str) -> str:
if not name or len(name) > 32:
raise ValueError('context menu names must be between 1-32 characters')
return name
def validate_auto_complete_callback(
callback: AutocompleteCallback[GroupT, ChoiceT]
) -> AutocompleteCallback[GroupT, ChoiceT]:
# This function needs to ensure the following is true:
# If self.foo is passed then don't pass command.binding to the callback
# If Class.foo is passed then it is assumed command.binding has to be passed
# If free_function_foo is passed then no binding should be passed at all
# Passing command.binding is mandated by pass_command_binding
binding = getattr(callback, '__self__', None)
pass_command_binding = binding is None and is_inside_class(callback)
# 'method' objects can't have dynamic attributes
if binding is None:
callback.pass_command_binding = pass_command_binding
required_parameters = 2 + pass_command_binding
params = inspect.signature(callback).parameters
if len(params) != required_parameters:
raise TypeError(f'autocomplete callback {callback.__qualname__!r} requires either 2 or 3 parameters to be passed')
return callback
def _context_menu_annotation(annotation: Any, *, _none: type = NoneType) -> AppCommandType:
if annotation is Message:
return AppCommandType.message
supported_types: Set[Any] = {Member, User}
if annotation in supported_types:
return AppCommandType.user
# Check if there's an origin
origin = getattr(annotation, '__origin__', None)
if origin is not Union:
# Only Union is supported so bail early
msg = (
f'unsupported type annotation {annotation!r}, must be either discord.Member, '
'discord.User, discord.Message, or a typing.Union of discord.Member and discord.User'
)
raise TypeError(msg)
# Only Union[Member, User] is supported
if not all(arg in supported_types for arg in annotation.__args__):
raise TypeError(f'unsupported types given inside {annotation!r}')
return AppCommandType.user
def _populate_descriptions(params: Dict[str, CommandParameter], descriptions: Dict[str, Any]) -> None:
for name, param in params.items():
description = descriptions.pop(name, MISSING)
if description is MISSING:
param.description = ''
continue
if not isinstance(description, (str, locale_str)):
raise TypeError('description must be a string')
if isinstance(description, str):
param.description = _shorten(description)
else:
param.description = description
if descriptions:
first = next(iter(descriptions))
raise TypeError(f'unknown parameter given: {first}')
def _populate_renames(params: Dict[str, CommandParameter], renames: Dict[str, Union[str, locale_str]]) -> None:
rename_map: Dict[str, Union[str, locale_str]] = {}
# original name to renamed name
for name in params.keys():
new_name = renames.pop(name, MISSING)
if new_name is MISSING:
rename_map[name] = name
continue
if name in rename_map:
raise ValueError(f'{new_name} is already used')
if isinstance(new_name, str):
new_name = validate_name(new_name)
else:
validate_name(new_name.message)
rename_map[name] = new_name
params[name]._rename = new_name
if renames:
first = next(iter(renames))
raise ValueError(f'unknown parameter given: {first}')
def _populate_choices(params: Dict[str, CommandParameter], all_choices: Dict[str, List[Choice]]) -> None:
for name, param in params.items():
choices = all_choices.pop(name, MISSING)
if choices is MISSING:
continue
if not isinstance(choices, list):
raise TypeError('choices must be a list of Choice')
if not all(isinstance(choice, Choice) for choice in choices):
raise TypeError('choices must be a list of Choice')
if param.type not in (AppCommandOptionType.string, AppCommandOptionType.number, AppCommandOptionType.integer):
raise TypeError('choices are only supported for integer, string, or number option types')
if not all(param.type == choice._option_type for choice in choices):
raise TypeError('choices must all have the same inner option type as the parameter choice type')
param.choices = choices
if all_choices:
first = next(iter(all_choices))
raise TypeError(f'unknown parameter given: {first}')
def _populate_autocomplete(params: Dict[str, CommandParameter], autocomplete: Dict[str, Any]) -> None:
for name, param in params.items():
callback = autocomplete.pop(name, MISSING)
if callback is MISSING:
continue
if not inspect.iscoroutinefunction(callback):
raise TypeError('autocomplete callback must be a coroutine function')
if param.type not in (AppCommandOptionType.string, AppCommandOptionType.number, AppCommandOptionType.integer):
raise TypeError('autocomplete is only supported for integer, string, or number option types')
if param.is_choice_annotation():
raise TypeError(
'Choice annotation unsupported for autocomplete parameters, consider using a regular annotation instead'
)
param.autocomplete = validate_auto_complete_callback(callback)
if autocomplete:
first = next(iter(autocomplete))
raise TypeError(f'unknown parameter given: {first}')
def _extract_parameters_from_callback(func: Callable[..., Any], globalns: Dict[str, Any]) -> Dict[str, CommandParameter]:
params = inspect.signature(func).parameters
cache = {}
required_params = is_inside_class(func) + 1
if len(params) < required_params:
raise TypeError(f'callback {func.__qualname__!r} must have more than {required_params - 1} parameter(s)')
iterator = iter(params.values())
for _ in range(0, required_params):
next(iterator)
parameters: List[CommandParameter] = []
for parameter in iterator:
if parameter.annotation is parameter.empty:
raise TypeError(f'parameter {parameter.name!r} is missing a type annotation in callback {func.__qualname__!r}')
resolved = resolve_annotation(parameter.annotation, globalns, globalns, cache)
param = annotation_to_parameter(resolved, parameter)
parameters.append(param)
values = sorted(parameters, key=lambda a: a.required, reverse=True)
result = {v.name: v for v in values}
descriptions = _parse_args_from_docstring(func, result)
try:
descriptions.update(func.__discord_app_commands_param_description__)
except AttributeError:
for param in values:
if param.description is MISSING:
param.description = ''
if descriptions:
_populate_descriptions(result, descriptions)
try:
renames = func.__discord_app_commands_param_rename__
except AttributeError:
pass
else:
_populate_renames(result, renames.copy())
try:
choices = func.__discord_app_commands_param_choices__
except AttributeError:
pass
else:
_populate_choices(result, choices.copy())
try:
autocomplete = func.__discord_app_commands_param_autocomplete__
except AttributeError:
pass
else:
_populate_autocomplete(result, autocomplete.copy())
return result
def _get_context_menu_parameter(func: ContextMenuCallback) -> Tuple[str, Any, AppCommandType]:
params = inspect.signature(func).parameters
if is_inside_class(func) and not hasattr(func, '__self__'):
raise TypeError('context menus cannot be defined inside a class')
if len(params) != 2:
msg = (
f'context menu callback {func.__qualname__!r} requires 2 parameters, '
'the first one being the interaction and the other one explicitly '
'annotated with either discord.Message, discord.User, discord.Member, '
'or a typing.Union of discord.Member and discord.User'
)
raise TypeError(msg)
iterator = iter(params.values())
next(iterator) # skip interaction
parameter = next(iterator)
if parameter.annotation is parameter.empty:
msg = (
f'second parameter of context menu callback {func.__qualname__!r} must be explicitly '
'annotated with either discord.Message, discord.User, discord.Member, or '
'a typing.Union of discord.Member and discord.User'
)
raise TypeError(msg)
resolved = resolve_annotation(parameter.annotation, func.__globals__, func.__globals__, {})
type = _context_menu_annotation(resolved)
return (parameter.name, resolved, type)
def mark_overrideable(func: F) -> F:
func.__discord_app_commands_base_function__ = None
return func
class Parameter:
"""A class that contains the parameter information of a :class:`Command` callback.
.. versionadded:: 2.0
Attributes
-----------
name: :class:`str`
The name of the parameter. This is the Python identifier for the parameter.
display_name: :class:`str`
The displayed name of the parameter on Discord.
description: :class:`str`
The description of the parameter.
autocomplete: :class:`bool`
Whether the parameter has an autocomplete handler.
locale_name: Optional[:class:`locale_str`]
The display name's locale string, if available.
locale_description: Optional[:class:`locale_str`]
The description's locale string, if available.
required: :class:`bool`
Whether the parameter is required
choices: List[:class:`~discord.app_commands.Choice`]
A list of choices this parameter takes, if any.
type: :class:`~discord.AppCommandOptionType`
The underlying type of this parameter.
channel_types: List[:class:`~discord.ChannelType`]
The channel types that are allowed for this parameter.
min_value: Optional[Union[:class:`int`, :class:`float`]]
The minimum supported value for this parameter.
max_value: Optional[Union[:class:`int`, :class:`float`]]
The maximum supported value for this parameter.
default: Any
The default value of the parameter, if given.
If not given then this is :data:`~discord.utils.MISSING`.
command: :class:`Command`
The command this parameter is attached to.
"""
def __init__(self, parent: CommandParameter, command: Command[Any, ..., Any]) -> None:
self.__parent: CommandParameter = parent
self.__command: Command[Any, ..., Any] = command
@property
def command(self) -> Command[Any, ..., Any]:
return self.__command
@property
def name(self) -> str:
return self.__parent.name
@property
def display_name(self) -> str:
return self.__parent.display_name
@property
def required(self) -> bool:
return self.__parent.required
@property
def description(self) -> str:
return str(self.__parent.description)
@property
def locale_name(self) -> Optional[locale_str]:
if isinstance(self.__parent._rename, locale_str):
return self.__parent._rename
return None
@property
def locale_description(self) -> Optional[locale_str]:
if isinstance(self.__parent.description, locale_str):
return self.__parent.description
return None
@property
def autocomplete(self) -> bool:
return self.__parent.autocomplete is not None
@property
def default(self) -> Any:
return self.__parent.default
@property
def type(self) -> AppCommandOptionType:
return self.__parent.type
@property
def choices(self) -> List[Choice[Union[int, float, str]]]:
choices = self.__parent.choices
if choices is MISSING:
return []
return choices.copy()
@property
def channel_types(self) -> List[ChannelType]:
channel_types = self.__parent.channel_types
if channel_types is MISSING:
return []
return channel_types.copy()
@property
def min_value(self) -> Optional[Union[int, float]]:
return self.__parent.min_value
@property
def max_value(self) -> Optional[Union[int, float]]:
return self.__parent.max_value
class Command(Generic[GroupT, P, T]):
"""A class that implements an application command.
These are usually not created manually, instead they are created using
one of the following decorators:
- :func:`~discord.app_commands.command`
- :meth:`Group.command <discord.app_commands.Group.command>`
- :meth:`CommandTree.command <discord.app_commands.CommandTree.command>`
.. versionadded:: 2.0
Parameters
-----------
name: Union[:class:`str`, :class:`locale_str`]
The name of the application command.
description: Union[:class:`str`, :class:`locale_str`]
The description of the application command. This shows up in the UI to describe
the application command.
callback: :ref:`coroutine <coroutine>`
The coroutine that is executed when the command is called.
auto_locale_strings: :class:`bool`
If this is set to ``True``, then all translatable strings will implicitly
be wrapped into :class:`locale_str` rather than :class:`str`. This could
avoid some repetition and be more ergonomic for certain defaults such
as default command names, command descriptions, and parameter names.
Defaults to ``True``.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
parent: Optional[:class:`Group`]
The parent application command. ``None`` if there isn't one.
extras: :class:`dict`
A dictionary that can be used to store extraneous data.
The library will not touch any values or keys within this dictionary.
Attributes
------------
name: :class:`str`
The name of the application command.
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.
default_permissions: Optional[:class:`~discord.Permissions`]
The default permissions that can execute this command on Discord. Note
that server administrators can override this value in the client.
Setting an empty permissions field will disallow anyone except server
administrators from using the command in a guild.
Due to a Discord limitation, this does not work on subcommands.
guild_only: :class:`bool`
Whether the command should only be usable in guild contexts.
Due to a Discord limitation, this does not work on subcommands.
allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`]
The contexts that the command is allowed to be used in.
Overrides ``guild_only`` if this is set.
.. versionadded:: 2.4
allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`]
The installation contexts that the command is allowed to be installed
on.
.. versionadded:: 2.4
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
Due to a Discord limitation, this does not work on subcommands.
parent: Optional[:class:`Group`]
The parent application command. ``None`` if there isn't one.
extras: :class:`dict`
A dictionary that can be used to store extraneous data.
The library will not touch any values or keys within this dictionary.
"""
def __init__(
self,
*,
name: Union[str, locale_str],
description: Union[str, locale_str],
callback: CommandCallback[GroupT, P, T],
nsfw: bool = False,
parent: Optional[Group] = None,
guild_ids: Optional[List[int]] = None,
allowed_contexts: Optional[AppCommandContext] = None,
allowed_installs: Optional[AppInstallationType] = None,
auto_locale_strings: bool = True,
extras: Dict[Any, Any] = MISSING,
):
name, locale = (name.message, name) if isinstance(name, locale_str) else (name, None)
self.name: str = validate_name(name)
self._locale_name: Optional[locale_str] = locale
description, locale = (
(description.message, description) if isinstance(description, locale_str) else (description, None)
)
self.description: str = description
self._locale_description: Optional[locale_str] = locale
self._attr: Optional[str] = None
self._callback: CommandCallback[GroupT, P, T] = callback
self.parent: Optional[Group] = parent
self.binding: Optional[GroupT] = None
self.on_error: Optional[Error[GroupT]] = None
self.module: Optional[str] = callback.__module__
# Unwrap __self__ for bound methods
try:
self.binding = callback.__self__
self._callback = callback = callback.__func__
except AttributeError:
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
)
self.default_permissions: Optional[Permissions] = getattr(
callback, '__discord_app_commands_default_permissions__', None
)
self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False)
self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts or getattr(
callback, '__discord_app_commands_contexts__', None
)
self.allowed_installs: Optional[AppInstallationType] = allowed_installs or getattr(
callback, '__discord_app_commands_installation_types__', None
)
self.nsfw: bool = nsfw
self.extras: Dict[Any, Any] = extras or {}
if self._guild_ids is not None and self.parent is not None:
raise ValueError('child commands cannot have default guilds set, consider setting them in the parent instead')
if auto_locale_strings:
self._convert_to_locale_strings()
def _convert_to_locale_strings(self) -> None:
if self._locale_name is None:
self._locale_name = locale_str(self.name)
if self._locale_description is None:
self._locale_description = locale_str(self.description)
for param in self._params.values():
param._convert_to_locale_strings()
def __set_name__(self, owner: Type[Any], name: str) -> None:
self._attr = name
@property
def callback(self) -> CommandCallback[GroupT, P, T]:
""":ref:`coroutine <coroutine>`: The coroutine that is executed when the command is called."""
return self._callback
def _copy_with(
self,
*,
parent: Optional[Group],
binding: GroupT,
bindings: MutableMapping[GroupT, GroupT] = MISSING,
set_on_binding: bool = True,
) -> Command:
bindings = {} if bindings is MISSING else bindings
copy = shallow_copy(self)
copy._params = self._params.copy()
copy.parent = parent
copy.binding = bindings.get(self.binding) if self.binding is not None else binding
if copy._attr and set_on_binding:
setattr(copy.binding, copy._attr, copy)
return copy
async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]:
base = self.to_dict(tree)
name_localizations: Dict[str, str] = {}
description_localizations: Dict[str, str] = {}
# Prevent creating these objects in a heavy loop
name_context = TranslationContext(location=TranslationContextLocation.command_name, data=self)
description_context = TranslationContext(location=TranslationContextLocation.command_description, data=self)
for locale in Locale:
if self._locale_name:
translation = await translator._checked_translate(self._locale_name, locale, name_context)
if translation is not None:
name_localizations[locale.value] = translation
if self._locale_description:
translation = await translator._checked_translate(self._locale_description, locale, description_context)
if translation is not None:
description_localizations[locale.value] = translation
base['name_localizations'] = name_localizations
base['description_localizations'] = description_localizations
base['options'] = [
await param.get_translated_payload(translator, Parameter(param, self)) for param in self._params.values()
]
return base
def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]:
# If we have a parent then our type is a subcommand
# Otherwise, the type falls back to the specific command type (e.g. slash command or context menu)
option_type = AppCommandType.chat_input.value if self.parent is None else AppCommandOptionType.subcommand.value
base: Dict[str, Any] = {
'name': self.name,
'description': self.description,
'type': option_type,
'options': [param.to_dict() for param in self._params.values()],
}
if self.parent is None:
base['nsfw'] = self.nsfw
base['dm_permission'] = not self.guild_only
base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value
base['contexts'] = tree.allowed_contexts._merge_to_array(self.allowed_contexts)
base['integration_types'] = tree.allowed_installs._merge_to_array(self.allowed_installs)
return base
async def _invoke_error_handlers(self, interaction: Interaction, error: AppCommandError) -> None:
# These type ignores are because the type checker can't narrow this type properly.
if self.on_error is not None:
if self.binding is not None:
await self.on_error(self.binding, interaction, error) # type: ignore
else:
await self.on_error(interaction, error) # type: ignore
parent = self.parent
if parent is not None:
await parent.on_error(interaction, error)
if parent.parent is not None:
await parent.parent.on_error(interaction, error)
binding_error_handler = getattr(self.binding, '__discord_app_commands_error_handler__', None)
if binding_error_handler is not None:
await binding_error_handler(interaction, error)
def _has_any_error_handlers(self) -> bool:
if self.on_error is not None:
return True
parent = self.parent
if parent is not None:
# Check if the on_error is overridden
if not hasattr(parent.on_error, '__discord_app_commands_base_function__'):
return True
if parent.parent is not None:
if not hasattr(parent.parent.on_error, '__discord_app_commands_base_function__'):
return True
# Check if we have a bound error handler
if getattr(self.binding, '__discord_app_commands_error_handler__', None) is not None:
return True
return False
async def _transform_arguments(self, interaction: Interaction, namespace: Namespace) -> Dict[str, Any]:
values = namespace.__dict__
transformed_values = {}
for param in self._params.values():
try:
value = values[param.display_name]
except KeyError:
if not param.required:
transformed_values[param.name] = param.default
else:
raise CommandSignatureMismatch(self) from None
else:
transformed_values[param.name] = await param.transform(interaction, value)
return transformed_values
async def _do_call(self, interaction: Interaction, params: Dict[str, Any]) -> T:
# These type ignores are because the type checker doesn't quite understand the narrowing here
# Likewise, it thinks we're missing positional arguments when there aren't any.
try:
if self.binding is not None:
return await self._callback(self.binding, interaction, **params) # type: ignore
return await self._callback(interaction, **params) # type: ignore
except TypeError as e:
# In order to detect mismatch from the provided signature and the Discord data,
# there are many ways it can go wrong yet all of them eventually lead to a TypeError
# from the Python compiler showcasing that the signature is incorrect. This lovely
# piece of code essentially checks the last frame of the caller and checks if the
# locals contains our `self` reference.
#
# This is because there is a possibility that a TypeError is raised within the body
# of the function, and in that case the locals wouldn't contain a reference to
# the command object under the name `self`.
frame = inspect.trace()[-1].frame
if frame.f_locals.get('self') is self:
raise CommandSignatureMismatch(self) from None
raise CommandInvokeError(self, e) from e
except AppCommandError:
raise
except Exception as e:
raise CommandInvokeError(self, e) from e
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.')
transformed_values = await self._transform_arguments(interaction, namespace)
return await self._do_call(interaction, transformed_values)
async def _invoke_autocomplete(self, interaction: Interaction, name: str, namespace: Namespace):
# The namespace contains the Discord provided names so this will be fine
# even if the name is renamed
value = namespace.__dict__[name]
try:
param = self._params[name]
except KeyError:
# Slow case, it might be a rename
params = {param.display_name: param for param in self._params.values()}
try:
param = params[name]
except KeyError:
raise CommandSignatureMismatch(self) from None
if param.autocomplete is None:
raise CommandSignatureMismatch(self)
predicates = getattr(param.autocomplete, '__discord_app_commands_checks__', [])
if predicates:
try:
passed = await async_all(f(interaction) for f in predicates) # type: ignore
except Exception:
passed = False
if not passed:
if not interaction.response.is_done():
await interaction.response.autocomplete([])
return
if getattr(param.autocomplete, 'pass_command_binding', False):
binding = self.binding
if binding is not None:
choices = await param.autocomplete(binding, interaction, value)
else:
raise TypeError('autocomplete parameter expected a bound self parameter but one was not provided')
else:
choices = await param.autocomplete(interaction, value)
if interaction.response.is_done():
return
await interaction.response.autocomplete(choices)
def _get_internal_command(self, name: str) -> Optional[Union[Command, Group]]:
return None
@property
def parameters(self) -> List[Parameter]:
"""Returns a list of parameters for this command.
This does not include the ``self`` or ``interaction`` parameters.
Returns
--------
List[:class:`Parameter`]
The parameters of this command.
"""
return [Parameter(p, self) for p in self._params.values()]
def get_parameter(self, name: str) -> Optional[Parameter]:
"""Retrieves a parameter by its name.
The name must be the Python identifier rather than the renamed
one for display on Discord.
Parameters
-----------
name: :class:`str`
The parameter name in the callback function.
Returns
--------
Optional[:class:`Parameter`]
The parameter or ``None`` if not found.
"""
parent = self._params.get(name)
if parent is not None:
return Parameter(parent, self)
return None
@property
def root_parent(self) -> Optional[Group]:
"""Optional[:class:`Group`]: The root parent of this command."""
if self.parent is None:
return None
parent = self.parent
return parent.parent or parent
@property
def qualified_name(self) -> str:
""":class:`str`: Returns the fully qualified command name.
The qualified name includes the parent name as well. For example,
in a command like ``/foo bar`` the qualified name is ``foo bar``.
"""
# A B C
# ^ self
# ^ parent
# ^ grandparent
if self.parent is None:
return self.name
names = [self.name, self.parent.name]
grandparent = self.parent.parent
if grandparent is not None:
names.append(grandparent.name)
return ' '.join(reversed(names))
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:
check: Optional[Check] = getattr(self.binding, 'interaction_check', None)
if check:
ret = await maybe_coroutine(check, interaction)
if not ret:
return False
predicates = self.checks
if not predicates:
return True
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.
The local error handler is called whenever an exception is raised in the body
of the command or during handling of the command. The error handler must take
2 parameters, the interaction and the error.
The error passed will be derived from :exc:`AppCommandError`.
Parameters
-----------
coro: :ref:`coroutine <coroutine>`
The coroutine to register as the local error handler.
Raises
-------
TypeError
The coroutine passed is not actually a coroutine.
"""
if not inspect.iscoroutinefunction(coro):
raise TypeError('The error handler must be a coroutine.')
self.on_error = coro
return coro
def autocomplete(
self, name: str
) -> Callable[[AutocompleteCallback[GroupT, ChoiceT]], AutocompleteCallback[GroupT, ChoiceT]]:
"""A decorator that registers a coroutine as an autocomplete prompt for a parameter.
The coroutine callback must have 2 parameters, the :class:`~discord.Interaction`,
and the current value by the user (the string currently being typed by the user).
To get the values from other parameters that may be filled in, accessing
:attr:`.Interaction.namespace` will give a :class:`Namespace` object with those
values.
Parent :func:`checks <check>` are ignored within an autocomplete. However, checks can be added
to the autocomplete callback and the ones added will be called. If the checks fail for any reason
then an empty list is sent as the interaction response.
The coroutine decorator **must** return a list of :class:`~discord.app_commands.Choice` objects.
Only up to 25 objects are supported.
.. warning::
The choices returned from this coroutine are suggestions. The user may ignore them and input their own value.
Example:
.. code-block:: python3
@app_commands.command()
async def fruits(interaction: discord.Interaction, fruit: str):
await interaction.response.send_message(f'Your favourite fruit seems to be {fruit}')
@fruits.autocomplete('fruit')
async def fruits_autocomplete(
interaction: discord.Interaction,
current: str,
) -> List[app_commands.Choice[str]]:
fruits = ['Banana', 'Pineapple', 'Apple', 'Watermelon', 'Melon', 'Cherry']
return [
app_commands.Choice(name=fruit, value=fruit)
for fruit in fruits if current.lower() in fruit.lower()
]
Parameters
-----------
name: :class:`str`
The parameter name to register as autocomplete.
Raises
-------
TypeError
The coroutine passed is not actually a coroutine or
the parameter is not found or of an invalid type.
"""
def decorator(coro: AutocompleteCallback[GroupT, ChoiceT]) -> AutocompleteCallback[GroupT, ChoiceT]:
if not inspect.iscoroutinefunction(coro):
raise TypeError('The autocomplete callback must be a coroutine function.')
try:
param = self._params[name]
except KeyError:
raise TypeError(f'unknown parameter: {name!r}') from None
if param.type not in (AppCommandOptionType.string, AppCommandOptionType.number, AppCommandOptionType.integer):
raise TypeError('autocomplete is only supported for integer, string, or number option types')
if param.is_choice_annotation():
raise TypeError(
'Choice annotation unsupported for autocomplete parameters, consider using a regular annotation instead'
)
param.autocomplete = validate_auto_complete_callback(coro)
return coro
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.
These are usually not created manually, instead they are created using
one of the following decorators:
- :func:`~discord.app_commands.context_menu`
- :meth:`CommandTree.context_menu <discord.app_commands.CommandTree.context_menu>`
.. versionadded:: 2.0
Parameters
-----------
name: Union[:class:`str`, :class:`locale_str`]
The name of the context menu.
callback: :ref:`coroutine <coroutine>`
The coroutine that is executed when the command is called.
type: :class:`.AppCommandType`
The type of context menu application command. By default, this is inferred
by the parameter of the callback.
auto_locale_strings: :class:`bool`
If this is set to ``True``, then all translatable strings will implicitly
be wrapped into :class:`locale_str` rather than :class:`str`. This could
avoid some repetition and be more ergonomic for certain defaults such
as default command names, command descriptions, and parameter names.
Defaults to ``True``.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
Defaults to ``False``.
extras: :class:`dict`
A dictionary that can be used to store extraneous data.
The library will not touch any values or keys within this dictionary.
Attributes
------------
name: :class:`str`
The name of the context menu.
type: :class:`.AppCommandType`
The type of context menu application command. By default, this is inferred
by the parameter of the callback.
default_permissions: Optional[:class:`~discord.Permissions`]
The default permissions that can execute this command on Discord. Note
that server administrators can override this value in the client.
Setting an empty permissions field will disallow anyone except server
administrators from using the command in a guild.
guild_only: :class:`bool`
Whether the command should only be usable in guild contexts.
Defaults to ``False``.
allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`]
The contexts that this context menu is allowed to be used in.
Overrides ``guild_only`` if set.
.. versionadded:: 2.4
allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`]
The installation contexts that the command is allowed to be installed
on.
.. versionadded:: 2.4
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
Defaults to ``False``.
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.
extras: :class:`dict`
A dictionary that can be used to store extraneous data.
The library will not touch any values or keys within this dictionary.
"""
def __init__(
self,
*,
name: Union[str, locale_str],
callback: ContextMenuCallback,
type: AppCommandType = MISSING,
nsfw: bool = False,
guild_ids: Optional[List[int]] = None,
allowed_contexts: Optional[AppCommandContext] = None,
allowed_installs: Optional[AppInstallationType] = None,
auto_locale_strings: bool = True,
extras: Dict[Any, Any] = MISSING,
):
name, locale = (name.message, name) if isinstance(name, locale_str) else (name, None)
self.name: str = validate_context_menu_name(name)
self._locale_name: Optional[locale_str] = locale
self._callback: ContextMenuCallback = callback
(param, annotation, actual_type) = _get_context_menu_parameter(callback)
if type is MISSING:
type = actual_type
if actual_type != type:
raise ValueError(f'context menu callback implies a type of {actual_type} but {type} was passed.')
self.type: AppCommandType = type
self._param_name = param
self._annotation = annotation
self.module: Optional[str] = callback.__module__
self._guild_ids = guild_ids or getattr(callback, '__discord_app_commands_default_guilds__', None)
self.on_error: Optional[UnboundError] = None
self.default_permissions: Optional[Permissions] = getattr(
callback, '__discord_app_commands_default_permissions__', None
)
self.nsfw: bool = nsfw
self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False)
self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts or getattr(
callback, '__discord_app_commands_contexts__', None
)
self.allowed_installs: Optional[AppInstallationType] = allowed_installs or getattr(
callback, '__discord_app_commands_installation_types__', None
)
self.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', [])
self.extras: Dict[Any, Any] = extras or {}
if auto_locale_strings:
if self._locale_name is None:
self._locale_name = locale_str(self.name)
@property
def callback(self) -> ContextMenuCallback:
""":ref:`coroutine <coroutine>`: The coroutine that is executed when the context menu is called."""
return self._callback
@property
def qualified_name(self) -> str:
""":class:`str`: Returns the fully qualified command name."""
return self.name
async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]:
base = self.to_dict(tree)
context = TranslationContext(location=TranslationContextLocation.command_name, data=self)
if self._locale_name:
name_localizations: Dict[str, str] = {}
for locale in Locale:
translation = await translator._checked_translate(self._locale_name, locale, context)
if translation is not None:
name_localizations[locale.value] = translation
base['name_localizations'] = name_localizations
return base
def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]:
return {
'name': self.name,
'type': self.type.value,
'dm_permission': not self.guild_only,
'contexts': tree.allowed_contexts._merge_to_array(self.allowed_contexts),
'integration_types': tree.allowed_installs._merge_to_array(self.allowed_installs),
'default_member_permissions': None if self.default_permissions is None else self.default_permissions.value,
'nsfw': self.nsfw,
}
async def _check_can_run(self, interaction: Interaction) -> bool:
predicates = self.checks
if not predicates:
return True
return await async_all(f(interaction) for f in predicates) # type: ignore
def _has_any_error_handlers(self) -> bool:
return self.on_error is not None
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 error(self, coro: UnboundError) -> UnboundError:
"""A decorator that registers a coroutine as a local error handler.
The local error handler is called whenever an exception is raised in the body
of the command or during handling of the command. The error handler must take
2 parameters, the interaction and the error.
The error passed will be derived from :exc:`AppCommandError`.
Parameters
-----------
coro: :ref:`coroutine <coroutine>`
The coroutine to register as the local error handler.
Raises
-------
TypeError
The coroutine passed is not actually a coroutine.
"""
if not inspect.iscoroutinefunction(coro):
raise TypeError('The error handler must be a coroutine.')
self.on_error = coro
return coro
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.
These are usually inherited rather than created manually.
Decorators such as :func:`guild_only`, :func:`guilds`, and :func:`default_permissions`
will apply to the group if used on top of a subclass. For example:
.. code-block:: python3
from discord import app_commands
@app_commands.guild_only()
class MyGroup(app_commands.Group):
pass
.. versionadded:: 2.0
Parameters
-----------
name: Union[:class:`str`, :class:`locale_str`]
The name of the group. If not given, it defaults to a lower-case
kebab-case version of the class name.
description: Union[:class:`str`, :class:`locale_str`]
The description of the group. This shows up in the UI to describe
the group. If not given, it defaults to the docstring of the
class shortened to 100 characters.
auto_locale_strings: :class:`bool`
If this is set to ``True``, then all translatable strings will implicitly
be wrapped into :class:`locale_str` rather than :class:`str`. This could
avoid some repetition and be more ergonomic for certain defaults such
as default command names, command descriptions, and parameter names.
Defaults to ``True``.
default_permissions: Optional[:class:`~discord.Permissions`]
The default permissions that can execute this group on Discord. Note
that server administrators can override this value in the client.
Setting an empty permissions field will disallow anyone except server
administrators from using the command in a guild.
Due to a Discord limitation, this does not work on subcommands.
guild_only: :class:`bool`
Whether the group should only be usable in guild contexts.
Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
parent: Optional[:class:`Group`]
The parent application command. ``None`` if there isn't one.
extras: :class:`dict`
A dictionary that can be used to store extraneous data.
The library will not touch any values or keys within this dictionary.
Attributes
------------
name: :class:`str`
The name of the group.
description: :class:`str`
The description of the group. This shows up in the UI to describe
the group.
default_permissions: Optional[:class:`~discord.Permissions`]
The default permissions that can execute this group on Discord. Note
that server administrators can override this value in the client.
Setting an empty permissions field will disallow anyone except server
administrators from using the command in a guild.
Due to a Discord limitation, this does not work on subcommands.
guild_only: :class:`bool`
Whether the group should only be usable in guild contexts.
Due to a Discord limitation, this does not work on subcommands.
allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`]
The contexts that this group is allowed to be used in. Overrides
guild_only if set.
.. versionadded:: 2.4
allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`]
The installation contexts that the command is allowed to be installed
on.
.. versionadded:: 2.4
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels.
Due to a Discord limitation, this does not work on subcommands.
parent: Optional[:class:`Group`]
The parent group. ``None`` if there isn't one.
extras: :class:`dict`
A dictionary that can be used to store extraneous data.
The library will not touch any values or keys within this dictionary.
"""
__discord_app_commands_group_children__: ClassVar[List[Union[Command[Any, ..., Any], Group]]] = []
__discord_app_commands_skip_init_binding__: bool = False
__discord_app_commands_group_name__: str = MISSING
__discord_app_commands_group_description__: str = MISSING
__discord_app_commands_group_locale_name__: Optional[locale_str] = None
__discord_app_commands_group_locale_description__: Optional[locale_str] = None
__discord_app_commands_group_nsfw__: bool = False
__discord_app_commands_guild_only__: bool = MISSING
__discord_app_commands_contexts__: Optional[AppCommandContext] = MISSING
__discord_app_commands_installation_types__: Optional[AppInstallationType] = MISSING
__discord_app_commands_default_permissions__: Optional[Permissions] = MISSING
__discord_app_commands_has_module__: bool = False
__discord_app_commands_error_handler__: Optional[
Callable[[Interaction, AppCommandError], Coroutine[Any, Any, None]]
] = None
def __init_subclass__(
cls,
*,
name: Union[str, locale_str] = MISSING,
description: Union[str, locale_str] = MISSING,
guild_only: bool = MISSING,
nsfw: bool = False,
default_permissions: Optional[Permissions] = MISSING,
) -> None:
if not cls.__discord_app_commands_group_children__:
children: List[Union[Command[Any, ..., Any], Group]] = [
member for member in cls.__dict__.values() if isinstance(member, (Group, Command)) and member.parent is None
]
cls.__discord_app_commands_group_children__ = children
found = set()
for child in children:
if child.name in found:
raise TypeError(f'Command {child.name!r} is a duplicate')
found.add(child.name)
if len(children) > 25:
raise TypeError('groups cannot have more than 25 commands')
if name is MISSING:
cls.__discord_app_commands_group_name__ = validate_name(_to_kebab_case(cls.__name__))
elif isinstance(name, str):
cls.__discord_app_commands_group_name__ = validate_name(name)
else:
cls.__discord_app_commands_group_name__ = validate_name(name.message)
cls.__discord_app_commands_group_locale_name__ = name
if description is MISSING:
if cls.__doc__ is None:
cls.__discord_app_commands_group_description__ = ''
else:
cls.__discord_app_commands_group_description__ = _shorten(cls.__doc__)
elif isinstance(description, str):
cls.__discord_app_commands_group_description__ = description
else:
cls.__discord_app_commands_group_description__ = description.message
cls.__discord_app_commands_group_locale_description__ = description
if guild_only is not MISSING:
cls.__discord_app_commands_guild_only__ = guild_only
if default_permissions is not MISSING:
cls.__discord_app_commands_default_permissions__ = default_permissions
if cls.__module__ != __name__:
cls.__discord_app_commands_has_module__ = True
cls.__discord_app_commands_group_nsfw__ = nsfw
def __init__(
self,
*,
name: Union[str, locale_str] = MISSING,
description: Union[str, locale_str] = MISSING,
parent: Optional[Group] = None,
guild_ids: Optional[List[int]] = None,
guild_only: bool = MISSING,
allowed_contexts: Optional[AppCommandContext] = MISSING,
allowed_installs: Optional[AppInstallationType] = MISSING,
nsfw: bool = MISSING,
auto_locale_strings: bool = True,
default_permissions: Optional[Permissions] = MISSING,
extras: Dict[Any, Any] = MISSING,
):
cls = self.__class__
if name is MISSING:
name, locale = cls.__discord_app_commands_group_name__, cls.__discord_app_commands_group_locale_name__
elif isinstance(name, str):
name, locale = validate_name(name), None
else:
name, locale = validate_name(name.message), name
self.name: str = name
self._locale_name: Optional[locale_str] = locale
if description is MISSING:
description, locale = (
cls.__discord_app_commands_group_description__,
cls.__discord_app_commands_group_locale_description__,
)
elif isinstance(description, str):
description, locale = description, None
else:
description, locale = description.message, description
self.description: str = description
self._locale_description: Optional[locale_str] = locale
self._attr: Optional[str] = None
self._owner_cls: Optional[Type[Any]] = None
self._guild_ids: Optional[List[int]] = guild_ids or getattr(cls, '__discord_app_commands_default_guilds__', None)
if default_permissions is MISSING:
if cls.__discord_app_commands_default_permissions__ is MISSING:
default_permissions = None
else:
default_permissions = cls.__discord_app_commands_default_permissions__
self.default_permissions: Optional[Permissions] = default_permissions
if guild_only is MISSING:
if cls.__discord_app_commands_guild_only__ is MISSING:
guild_only = False
else:
guild_only = cls.__discord_app_commands_guild_only__
self.guild_only: bool = guild_only
if allowed_contexts is MISSING:
if cls.__discord_app_commands_contexts__ is MISSING:
allowed_contexts = None
else:
allowed_contexts = cls.__discord_app_commands_contexts__
self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts
if allowed_installs is MISSING:
if cls.__discord_app_commands_installation_types__ is MISSING:
allowed_installs = None
else:
allowed_installs = cls.__discord_app_commands_installation_types__
self.allowed_installs: Optional[AppInstallationType] = allowed_installs
if nsfw is MISSING:
nsfw = cls.__discord_app_commands_group_nsfw__
self.nsfw: bool = nsfw
if not self.description:
raise TypeError('groups must have a description')
if not self.name:
raise TypeError('groups must have a name')
self.parent: Optional[Group] = parent
self.module: Optional[str]
if cls.__discord_app_commands_has_module__:
self.module = cls.__module__
else:
try:
# This is pretty hacky
# It allows the module to be fetched if someone just constructs a bare Group object though.
self.module = inspect.currentframe().f_back.f_globals['__name__'] # type: ignore
except (AttributeError, IndexError, KeyError):
self.module = None
self._children: Dict[str, Union[Command, Group]] = {}
self.extras: Dict[Any, Any] = extras or {}
bindings: Dict[Group, Group] = {}
for child in self.__discord_app_commands_group_children__:
# commands and groups created directly in this class (no parent)
copy = (
child._copy_with(parent=self, binding=self, bindings=bindings, set_on_binding=False)
if not cls.__discord_app_commands_skip_init_binding__
else child
)
self._children[copy.name] = copy
if copy._attr and not cls.__discord_app_commands_skip_init_binding__:
setattr(self, copy._attr, copy)
if parent is not None:
if parent.parent is not None:
raise ValueError('groups can only be nested at most one level')
parent.add_command(self)
if auto_locale_strings:
self._convert_to_locale_strings()
def _convert_to_locale_strings(self) -> None:
if self._locale_name is None:
self._locale_name = locale_str(self.name)
if self._locale_description is None:
self._locale_description = locale_str(self.description)
# I don't know if propagating to the children is the right behaviour here.
def __set_name__(self, owner: Type[Any], name: str) -> None:
self._attr = name
self.module = owner.__module__
self._owner_cls = owner
def _copy_with(
self,
*,
parent: Optional[Group],
binding: Binding,
bindings: MutableMapping[Group, Group] = MISSING,
set_on_binding: bool = True,
) -> Group:
bindings = {} if bindings is MISSING else bindings
copy = shallow_copy(self)
copy.parent = parent
copy._children = {}
bindings[self] = copy
for child in self._children.values():
child_copy = child._copy_with(parent=copy, binding=binding, bindings=bindings)
child_copy.parent = copy
copy._children[child_copy.name] = child_copy
if isinstance(child_copy, Group) and child_copy._attr and set_on_binding:
if binding.__class__ is child_copy._owner_cls:
setattr(binding, child_copy._attr, child_copy)
elif child_copy._owner_cls is copy.__class__:
setattr(copy, child_copy._attr, child_copy)
if copy._attr and set_on_binding:
setattr(parent or binding, copy._attr, copy)
return copy
async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]:
base = self.to_dict(tree)
name_localizations: Dict[str, str] = {}
description_localizations: Dict[str, str] = {}
# Prevent creating these objects in a heavy loop
name_context = TranslationContext(location=TranslationContextLocation.group_name, data=self)
description_context = TranslationContext(location=TranslationContextLocation.group_description, data=self)
for locale in Locale:
if self._locale_name:
translation = await translator._checked_translate(self._locale_name, locale, name_context)
if translation is not None:
name_localizations[locale.value] = translation
if self._locale_description:
translation = await translator._checked_translate(self._locale_description, locale, description_context)
if translation is not None:
description_localizations[locale.value] = translation
base['name_localizations'] = name_localizations
base['description_localizations'] = description_localizations
base['options'] = [await child.get_translated_payload(tree, translator) for child in self._children.values()]
return base
def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]:
# If this has a parent command then it's part of a subcommand group
# Otherwise, it's just a regular command
option_type = 1 if self.parent is None else AppCommandOptionType.subcommand_group.value
base: Dict[str, Any] = {
'name': self.name,
'description': self.description,
'type': option_type,
'options': [child.to_dict(tree) for child in self._children.values()],
}
if self.parent is None:
base['nsfw'] = self.nsfw
base['dm_permission'] = not self.guild_only
base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value
base['contexts'] = tree.allowed_contexts._merge_to_array(self.allowed_contexts)
base['integration_types'] = tree.allowed_installs._merge_to_array(self.allowed_installs)
return base
@property
def root_parent(self) -> Optional[Group]:
"""Optional[:class:`Group`]: The parent of this group."""
return self.parent
@property
def qualified_name(self) -> str:
""":class:`str`: Returns the fully qualified group name.
The qualified name includes the parent name as well. For example,
in a group like ``/foo bar`` the qualified name is ``foo bar``.
"""
if self.parent is None:
return self.name
return f'{self.parent.name} {self.name}'
def _get_internal_command(self, name: str) -> Optional[Union[Command[Any, ..., Any], Group]]:
return self._children.get(name)
@property
def commands(self) -> List[Union[Command[Any, ..., Any], Group]]:
"""List[Union[:class:`Command`, :class:`Group`]]: The commands that this group contains."""
return list(self._children.values())
def walk_commands(self) -> Generator[Union[Command[Any, ..., Any], Group], None, None]:
"""An iterator that recursively walks through all commands that this group contains.
Yields
---------
Union[:class:`Command`, :class:`Group`]
The commands in this group.
"""
for command in self._children.values():
yield command
if isinstance(command, Group):
yield from command.walk_commands()
@mark_overrideable
async def on_error(self, interaction: Interaction, error: AppCommandError, /) -> None:
"""|coro|
A callback that is called when a child's command raises an :exc:`AppCommandError`.
To get the command that failed, :attr:`discord.Interaction.command` should be used.
The default implementation does nothing.
Parameters
-----------
interaction: :class:`~discord.Interaction`
The interaction that is being handled.
error: :exc:`AppCommandError`
The exception that was raised.
"""
pass
def error(self, coro: ErrorFunc) -> ErrorFunc:
"""A decorator that registers a coroutine as a local error handler.
The local error handler is called whenever an exception is raised in a child command.
The error handler must take 2 parameters, the interaction and the error.
The error passed will be derived from :exc:`AppCommandError`.
Parameters
-----------
coro: :ref:`coroutine <coroutine>`
The coroutine to register as the local error handler.
Raises
-------
TypeError
The coroutine passed is not actually a coroutine, or is an invalid coroutine.
"""
if not inspect.iscoroutinefunction(coro):
raise TypeError('The error handler must be a coroutine.')
params = inspect.signature(coro).parameters
if len(params) != 2:
raise TypeError('The error handler must have 2 parameters.')
self.on_error = coro # type: ignore
return coro
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.
Parameters
-----------
command: Union[:class:`Command`, :class:`Group`]
The command or group to add.
override: :class:`bool`
Whether to override a pre-existing command or group with the same name.
If ``False`` then an exception is raised.
Raises
-------
CommandAlreadyRegistered
The command or group is already registered. Note that the :attr:`CommandAlreadyRegistered.guild_id`
attribute will always be ``None`` in this case.
ValueError
There are too many commands already registered or the group is too
deeply nested.
TypeError
The wrong command type was passed.
"""
if not isinstance(command, (Command, Group)):
raise TypeError(f'expected Command or Group not {command.__class__.__name__}')
if isinstance(command, Group) and self.parent is not None:
# In a tree like so:
# <group>
# <self>
# <group>
# this needs to be forbidden
raise ValueError(f'{command.name!r} is too nested, groups can only be nested at most one level')
if not override and command.name in self._children:
raise CommandAlreadyRegistered(command.name, guild_id=None)
self._children[command.name] = command
command.parent = self
if len(self._children) > 25:
raise ValueError('maximum number of child commands exceeded')
def remove_command(self, name: str, /) -> Optional[Union[Command[Any, ..., Any], Group]]:
"""Removes a command or group from the internal list of commands.
Parameters
-----------
name: :class:`str`
The name of the command or group to remove.
Returns
--------
Optional[Union[:class:`~discord.app_commands.Command`, :class:`~discord.app_commands.Group`]]
The command that was removed. If nothing was removed
then ``None`` is returned instead.
"""
self._children.pop(name, None)
def get_command(self, name: str, /) -> Optional[Union[Command[Any, ..., Any], Group]]:
"""Retrieves a command or group from its name.
Parameters
-----------
name: :class:`str`
The name of the command or group to retrieve.
Returns
--------
Optional[Union[:class:`~discord.app_commands.Command`, :class:`~discord.app_commands.Group`]]
The command or group that was retrieved. If nothing was found
then ``None`` is returned instead.
"""
return self._children.get(name)
def command(
self,
*,
name: Union[str, locale_str] = MISSING,
description: Union[str, locale_str] = MISSING,
nsfw: bool = False,
auto_locale_strings: bool = True,
extras: Dict[Any, Any] = MISSING,
) -> Callable[[CommandCallback[GroupT, P, T]], Command[GroupT, P, T]]:
"""A decorator that creates an application command from a regular function under this group.
Parameters
------------
name: Union[:class:`str`, :class:`locale_str`]
The name of the application command. If not given, it defaults to a lower-case
version of the callback name.
description: Union[:class:`str`, :class:`locale_str`]
The description of the application command. This shows up in the UI to describe
the application command. If not given, it defaults to the first line of the docstring
of the callback shortened to 100 characters.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``.
auto_locale_strings: :class:`bool`
If this is set to ``True``, then all translatable strings will implicitly
be wrapped into :class:`locale_str` rather than :class:`str`. This could
avoid some repetition and be more ergonomic for certain defaults such
as default command names, command descriptions, and parameter names.
Defaults to ``True``.
extras: :class:`dict`
A dictionary that can be used to store extraneous data.
The library will not touch any values or keys within this dictionary.
"""
def decorator(func: CommandCallback[GroupT, P, T]) -> Command[GroupT, P, T]:
if not inspect.iscoroutinefunction(func):
raise TypeError('command function must be a coroutine function')
if description is MISSING:
if func.__doc__ is None:
desc = ''
else:
desc = _shorten(func.__doc__)
else:
desc = description
command = Command(
name=name if name is not MISSING else func.__name__,
description=desc,
callback=func,
nsfw=nsfw,
parent=self,
auto_locale_strings=auto_locale_strings,
extras=extras,
)
self.add_command(command)
return command
return decorator
def command(
*,
name: Union[str, locale_str] = MISSING,
description: Union[str, locale_str] = MISSING,
nsfw: bool = False,
auto_locale_strings: bool = True,
extras: Dict[Any, Any] = MISSING,
) -> Callable[[CommandCallback[GroupT, P, T]], Command[GroupT, P, T]]:
"""Creates an application command from a regular function.
Parameters
------------
name: :class:`str`
The name of the application command. If not given, it defaults to a lower-case
version of the callback name.
description: :class:`str`
The description of the application command. This shows up in the UI to describe
the application command. If not given, it defaults to the first line of the docstring
of the callback shortened to 100 characters.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
auto_locale_strings: :class:`bool`
If this is set to ``True``, then all translatable strings will implicitly
be wrapped into :class:`locale_str` rather than :class:`str`. This could
avoid some repetition and be more ergonomic for certain defaults such
as default command names, command descriptions, and parameter names.
Defaults to ``True``.
extras: :class:`dict`
A dictionary that can be used to store extraneous data.
The library will not touch any values or keys within this dictionary.
"""
def decorator(func: CommandCallback[GroupT, P, T]) -> Command[GroupT, P, T]:
if not inspect.iscoroutinefunction(func):
raise TypeError('command function must be a coroutine function')
if description is MISSING:
if func.__doc__ is None:
desc = ''
else:
desc = _shorten(func.__doc__)
else:
desc = description
return Command(
name=name if name is not MISSING else func.__name__,
description=desc,
callback=func,
parent=None,
nsfw=nsfw,
auto_locale_strings=auto_locale_strings,
extras=extras,
)
return decorator
def context_menu(
*,
name: Union[str, locale_str] = MISSING,
nsfw: bool = False,
auto_locale_strings: bool = True,
extras: Dict[Any, Any] = MISSING,
) -> Callable[[ContextMenuCallback], ContextMenu]:
"""Creates an application command context menu from a regular function.
This function must have a signature of :class:`~discord.Interaction` as its first parameter
and taking either a :class:`~discord.Member`, :class:`~discord.User`, or :class:`~discord.Message`,
or a :obj:`typing.Union` of ``Member`` and ``User`` as its second parameter.
Examples
---------
.. code-block:: python3
@app_commands.context_menu()
async def react(interaction: discord.Interaction, message: discord.Message):
await interaction.response.send_message('Very cool message!', ephemeral=True)
@app_commands.context_menu()
async def ban(interaction: discord.Interaction, user: discord.Member):
await interaction.response.send_message(f'Should I actually ban {user}...', ephemeral=True)
Parameters
------------
name: Union[:class:`str`, :class:`locale_str`]
The name of the context menu command. If not given, it defaults to a title-case
version of the callback name. Note that unlike regular slash commands this can
have spaces and upper case characters in the name.
nsfw: :class:`bool`
Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``.
Due to a Discord limitation, this does not work on subcommands.
auto_locale_strings: :class:`bool`
If this is set to ``True``, then all translatable strings will implicitly
be wrapped into :class:`locale_str` rather than :class:`str`. This could
avoid some repetition and be more ergonomic for certain defaults such
as default command names, command descriptions, and parameter names.
Defaults to ``True``.
extras: :class:`dict`
A dictionary that can be used to store extraneous data.
The library will not touch any values or keys within this dictionary.
"""
def decorator(func: ContextMenuCallback) -> ContextMenu:
if not inspect.iscoroutinefunction(func):
raise TypeError('context menu function must be a coroutine function')
actual_name = func.__name__.title() if name is MISSING else name
return ContextMenu(
name=actual_name,
nsfw=nsfw,
callback=func,
auto_locale_strings=auto_locale_strings,
extras=extras,
)
return decorator
def describe(**parameters: Union[str, locale_str]) -> Callable[[T], T]:
r'''Describes the given parameters by their name using the key of the keyword argument
as the name.
Example:
.. code-block:: python3
@app_commands.command(description='Bans a member')
@app_commands.describe(member='the member to ban')
async def ban(interaction: discord.Interaction, member: discord.Member):
await interaction.response.send_message(f'Banned {member}')
Alternatively, you can describe parameters using Google, Sphinx, or Numpy style docstrings.
Example:
.. code-block:: python3
@app_commands.command()
async def ban(interaction: discord.Interaction, member: discord.Member):
"""Bans a member
Parameters
-----------
member: discord.Member
the member to ban
"""
await interaction.response.send_message(f'Banned {member}')
Parameters
-----------
\*\*parameters: Union[:class:`str`, :class:`locale_str`]
The description of the parameters.
Raises
--------
TypeError
The parameter name is not found.
'''
def decorator(inner: T) -> T:
if isinstance(inner, Command):
_populate_descriptions(inner._params, parameters)
else:
try:
inner.__discord_app_commands_param_description__.update(parameters) # type: ignore # Runtime attribute access
except AttributeError:
inner.__discord_app_commands_param_description__ = parameters # type: ignore # Runtime attribute assignment
return inner
return decorator
def rename(**parameters: Union[str, locale_str]) -> Callable[[T], T]:
r"""Renames the given parameters by their name using the key of the keyword argument
as the name.
This renames the parameter within the Discord UI. When referring to the parameter in other
decorators, the parameter name used in the function is used instead of the renamed one.
Example:
.. code-block:: python3
@app_commands.command()
@app_commands.rename(the_member_to_ban='member')
async def ban(interaction: discord.Interaction, the_member_to_ban: discord.Member):
await interaction.response.send_message(f'Banned {the_member_to_ban}')
Parameters
-----------
\*\*parameters: Union[:class:`str`, :class:`locale_str`]
The name of the parameters.
Raises
--------
ValueError
The parameter name is already used by another parameter.
TypeError
The parameter name is not found.
"""
def decorator(inner: T) -> T:
if isinstance(inner, Command):
_populate_renames(inner._params, parameters)
else:
try:
inner.__discord_app_commands_param_rename__.update(parameters) # type: ignore # Runtime attribute access
except AttributeError:
inner.__discord_app_commands_param_rename__ = parameters # type: ignore # Runtime attribute assignment
return inner
return decorator
def choices(**parameters: List[Choice[ChoiceT]]) -> Callable[[T], T]:
r"""Instructs the given parameters by their name to use the given choices for their choices.
Example:
.. code-block:: python3
@app_commands.command()
@app_commands.describe(fruits='fruits to choose from')
@app_commands.choices(fruits=[
Choice(name='apple', value=1),
Choice(name='banana', value=2),
Choice(name='cherry', value=3),
])
async def fruit(interaction: discord.Interaction, fruits: Choice[int]):
await interaction.response.send_message(f'Your favourite fruit is {fruits.name}.')
.. note::
This is not the only way to provide choices to a command. There are two more ergonomic ways
of doing this. The first one is to use a :obj:`typing.Literal` annotation:
.. code-block:: python3
@app_commands.command()
@app_commands.describe(fruits='fruits to choose from')
async def fruit(interaction: discord.Interaction, fruits: Literal['apple', 'banana', 'cherry']):
await interaction.response.send_message(f'Your favourite fruit is {fruits}.')
The second way is to use an :class:`enum.Enum`:
.. code-block:: python3
class Fruits(enum.Enum):
apple = 1
banana = 2
cherry = 3
@app_commands.command()
@app_commands.describe(fruits='fruits to choose from')
async def fruit(interaction: discord.Interaction, fruits: Fruits):
await interaction.response.send_message(f'Your favourite fruit is {fruits}.')
Parameters
-----------
\*\*parameters
The choices of the parameters.
Raises
--------
TypeError
The parameter name is not found or the parameter type was incorrect.
"""
def decorator(inner: T) -> T:
if isinstance(inner, Command):
_populate_choices(inner._params, parameters)
else:
try:
inner.__discord_app_commands_param_choices__.update(parameters) # type: ignore # Runtime attribute access
except AttributeError:
inner.__discord_app_commands_param_choices__ = parameters # type: ignore # Runtime attribute assignment
return inner
return decorator
def autocomplete(**parameters: AutocompleteCallback[GroupT, ChoiceT]) -> Callable[[T], T]:
r"""Associates the given parameters with the given autocomplete callback.
Autocomplete is only supported on types that have :class:`str`, :class:`int`, or :class:`float`
values.
:func:`Checks <check>` are supported, however they must be attached to the autocomplete
callback in order to work. Checks attached to the command are ignored when invoking the autocomplete
callback.
For more information, see the :meth:`Command.autocomplete` documentation.
.. warning::
The choices returned from this coroutine are suggestions. The user may ignore them and input their own value.
Example:
.. code-block:: python3
async def fruit_autocomplete(
interaction: discord.Interaction,
current: str,
) -> List[app_commands.Choice[str]]:
fruits = ['Banana', 'Pineapple', 'Apple', 'Watermelon', 'Melon', 'Cherry']
return [
app_commands.Choice(name=fruit, value=fruit)
for fruit in fruits if current.lower() in fruit.lower()
]
@app_commands.command()
@app_commands.autocomplete(fruit=fruit_autocomplete)
async def fruits(interaction: discord.Interaction, fruit: str):
await interaction.response.send_message(f'Your favourite fruit seems to be {fruit}')
Parameters
-----------
\*\*parameters
The parameters to mark as autocomplete.
Raises
--------
TypeError
The parameter name is not found or the parameter type was incorrect.
"""
def decorator(inner: T) -> T:
if isinstance(inner, Command):
_populate_autocomplete(inner._params, parameters)
else:
try:
inner.__discord_app_commands_param_autocomplete__.update(parameters) # type: ignore # Runtime attribute access
except AttributeError:
inner.__discord_app_commands_param_autocomplete__ = parameters # type: ignore # Runtime attribute assignment
return inner
return decorator
def guilds(*guild_ids: Union[Snowflake, int]) -> Callable[[T], T]:
r"""Associates the given guilds with the command.
When the command instance is added to a :class:`CommandTree`, the guilds that are
specified by this decorator become the default guilds that it's added to rather
than being a global command.
.. note::
Due to an implementation quirk and Python limitation, if this is used in conjunction
with the :meth:`CommandTree.command` or :meth:`CommandTree.context_menu` decorator
then this must go below that decorator.
.. note ::
Due to a Discord limitation, this decorator cannot be used in conjunction with
contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types
(e.g. :func:`.app_commands.allowed_installs`).
Example:
.. code-block:: python3
MY_GUILD_ID = discord.Object(...) # Guild ID here
@app_commands.command()
@app_commands.guilds(MY_GUILD_ID)
async def bonk(interaction: discord.Interaction):
await interaction.response.send_message('Bonk', ephemeral=True)
Parameters
-----------
\*guild_ids: Union[:class:`int`, :class:`~discord.abc.Snowflake`]
The guilds to associate this command with. The command tree will
use this as the default when added rather than adding it as a global
command.
"""
defaults: List[int] = [g if isinstance(g, int) else g.id for g in guild_ids]
def decorator(inner: T) -> T:
if isinstance(inner, (Group, ContextMenu)):
inner._guild_ids = defaults
elif isinstance(inner, Command):
if inner.parent is not None:
raise ValueError('child commands of a group cannot have default guilds set')
inner._guild_ids = defaults
else:
# Runtime attribute assignment
inner.__discord_app_commands_default_guilds__ = defaults # type: ignore
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 app_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
@overload
def guild_only(func: None = ...) -> Callable[[T], T]:
...
@overload
def guild_only(func: T) -> T:
...
def guild_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
"""A decorator that indicates this command can only be used in a guild context.
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
Therefore, there is no error handler called when a command is used within a private message.
This decorator can be called with or without parentheses.
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
Examples
---------
.. code-block:: python3
@app_commands.command()
@app_commands.guild_only()
async def my_guild_only_command(interaction: discord.Interaction) -> None:
await interaction.response.send_message('I am only available in guilds!')
"""
def inner(f: T) -> T:
if isinstance(f, (Command, Group, ContextMenu)):
f.guild_only = True
allowed_contexts = f.allowed_contexts or AppCommandContext()
f.allowed_contexts = allowed_contexts
else:
f.__discord_app_commands_guild_only__ = True # type: ignore # Runtime attribute assignment
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
allowed_contexts.guild = True
return f
# Check if called with parentheses or not
if func is None:
# Called with parentheses
return inner
else:
return inner(func)
@overload
def private_channel_only(func: None = ...) -> Callable[[T], T]:
...
@overload
def private_channel_only(func: T) -> T:
...
def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
"""A decorator that indicates this command can only be used in the context of DMs and group DMs.
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
Therefore, there is no error handler called when a command is used within a guild.
This decorator can be called with or without parentheses.
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
.. versionadded:: 2.4
Examples
---------
.. code-block:: python3
@app_commands.command()
@app_commands.private_channel_only()
async def my_private_channel_only_command(interaction: discord.Interaction) -> None:
await interaction.response.send_message('I am only available in DMs and GDMs!')
"""
def inner(f: T) -> T:
if isinstance(f, (Command, Group, ContextMenu)):
f.guild_only = False
allowed_contexts = f.allowed_contexts or AppCommandContext()
f.allowed_contexts = allowed_contexts
else:
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
allowed_contexts.private_channel = True
return f
# Check if called with parentheses or not
if func is None:
# Called with parentheses
return inner
else:
return inner(func)
@overload
def dm_only(func: None = ...) -> Callable[[T], T]:
...
@overload
def dm_only(func: T) -> T:
...
def dm_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
"""A decorator that indicates this command can only be used in the context of bot DMs.
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
Therefore, there is no error handler called when a command is used within a guild or group DM.
This decorator can be called with or without parentheses.
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
Examples
---------
.. code-block:: python3
@app_commands.command()
@app_commands.dm_only()
async def my_dm_only_command(interaction: discord.Interaction) -> None:
await interaction.response.send_message('I am only available in DMs!')
"""
def inner(f: T) -> T:
if isinstance(f, (Command, Group, ContextMenu)):
f.guild_only = False
allowed_contexts = f.allowed_contexts or AppCommandContext()
f.allowed_contexts = allowed_contexts
else:
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
allowed_contexts.dm_channel = True
return f
# Check if called with parentheses or not
if func is None:
# Called with parentheses
return inner
else:
return inner(func)
def allowed_contexts(guilds: bool = MISSING, dms: bool = MISSING, private_channels: bool = MISSING) -> Callable[[T], T]:
"""A decorator that indicates this command can only be used in certain contexts.
Valid contexts are guilds, DMs and private channels.
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
.. versionadded:: 2.4
Examples
---------
.. code-block:: python3
@app_commands.command()
@app_commands.allowed_contexts(guilds=True, dms=False, private_channels=True)
async def my_command(interaction: discord.Interaction) -> None:
await interaction.response.send_message('I am only available in guilds and private channels!')
"""
def inner(f: T) -> T:
if isinstance(f, (Command, Group, ContextMenu)):
f.guild_only = False
allowed_contexts = f.allowed_contexts or AppCommandContext()
f.allowed_contexts = allowed_contexts
else:
allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext()
f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment
if guilds is not MISSING:
allowed_contexts.guild = guilds
if dms is not MISSING:
allowed_contexts.dm_channel = dms
if private_channels is not MISSING:
allowed_contexts.private_channel = private_channels
return f
return inner
@overload
def guild_install(func: None = ...) -> Callable[[T], T]:
...
@overload
def guild_install(func: T) -> T:
...
def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
"""A decorator that indicates this command should be installed in guilds.
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
.. versionadded:: 2.4
Examples
---------
.. code-block:: python3
@app_commands.command()
@app_commands.guild_install()
async def my_guild_install_command(interaction: discord.Interaction) -> None:
await interaction.response.send_message('I am installed in guilds by default!')
"""
def inner(f: T) -> T:
if isinstance(f, (Command, Group, ContextMenu)):
allowed_installs = f.allowed_installs or AppInstallationType()
f.allowed_installs = allowed_installs
else:
allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType()
f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment
allowed_installs.guild = True
return f
# Check if called with parentheses or not
if func is None:
# Called with parentheses
return inner
else:
return inner(func)
@overload
def user_install(func: None = ...) -> Callable[[T], T]:
...
@overload
def user_install(func: T) -> T:
...
def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]:
"""A decorator that indicates this command should be installed for users.
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
.. versionadded:: 2.4
Examples
---------
.. code-block:: python3
@app_commands.command()
@app_commands.user_install()
async def my_user_install_command(interaction: discord.Interaction) -> None:
await interaction.response.send_message('I am installed in users by default!')
"""
def inner(f: T) -> T:
if isinstance(f, (Command, Group, ContextMenu)):
allowed_installs = f.allowed_installs or AppInstallationType()
f.allowed_installs = allowed_installs
else:
allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType()
f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment
allowed_installs.user = True
return f
# Check if called with parentheses or not
if func is None:
# Called with parentheses
return inner
else:
return inner(func)
def allowed_installs(
guilds: bool = MISSING,
users: bool = MISSING,
) -> Callable[[T], T]:
"""A decorator that indicates this command should be installed in certain contexts.
Valid contexts are guilds and users.
This is **not** implemented as a :func:`check`, and is instead verified by Discord server side.
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
.. versionadded:: 2.4
Examples
---------
.. code-block:: python3
@app_commands.command()
@app_commands.allowed_installs(guilds=False, users=True)
async def my_command(interaction: discord.Interaction) -> None:
await interaction.response.send_message('I am installed in users by default!')
"""
def inner(f: T) -> T:
if isinstance(f, (Command, Group, ContextMenu)):
allowed_installs = f.allowed_installs or AppInstallationType()
f.allowed_installs = allowed_installs
else:
allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType()
f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment
if guilds is not MISSING:
allowed_installs.guild = guilds
if users is not MISSING:
allowed_installs.user = users
return f
return inner
def default_permissions(perms_obj: Optional[Permissions] = None, /, **perms: bool) -> Callable[[T], T]:
r"""A decorator that sets the default permissions needed to execute this command.
When this decorator is used, by default users must have these permissions to execute the command.
However, an administrator can change the permissions needed to execute this command using the official
client. Therefore, this only serves as a hint.
Setting an empty permissions field, including via calling this with no arguments, will disallow anyone
except server administrators from using the command in a guild.
This is sent to Discord server side, and is not a :func:`check`. Therefore, error handlers are not called.
Due to a Discord limitation, this decorator does nothing in subcommands and is ignored.
.. warning::
This serves as a *hint* and members are *not* required to have the permissions given to actually
execute this command. If you want to ensure that members have the permissions needed, consider using
:func:`~discord.app_commands.checks.has_permissions` instead.
Parameters
-----------
\*\*perms: :class:`bool`
Keyword arguments denoting the permissions to set as the default.
perms_obj: :class:`~discord.Permissions`
A permissions object as positional argument. This can be used in combination with ``**perms``.
.. versionadded:: 2.5
Examples
---------
.. code-block:: python3
@app_commands.command()
@app_commands.default_permissions(manage_messages=True)
async def test(interaction: discord.Interaction):
await interaction.response.send_message('You may or may not have manage messages.')
.. code-block:: python3
ADMIN_PERMS = discord.Permissions(administrator=True)
@app_commands.command()
@app_commands.default_permissions(ADMIN_PERMS, manage_messages=True)
async def test(interaction: discord.Interaction):
await interaction.response.send_message('You may or may not have manage messages.')
"""
if perms_obj is not None:
permissions = perms_obj | Permissions(**perms)
else:
permissions = Permissions(**perms)
def decorator(func: T) -> T:
if isinstance(func, (Command, Group, ContextMenu)):
func.default_permissions = permissions
else:
func.__discord_app_commands_default_permissions__ = permissions # type: ignore # Runtime attribute assignment
return func
return decorator