10 changed files with 2083 additions and 2 deletions
@ -0,0 +1,16 @@ |
|||
""" |
|||
discord.app_commands |
|||
~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
Application commands support for the Discord API |
|||
|
|||
:copyright: (c) 2015-present Rapptz |
|||
:license: MIT, see LICENSE for more details. |
|||
|
|||
""" |
|||
|
|||
from .commands import * |
|||
from .enums import * |
|||
from .errors import * |
|||
from .models import * |
|||
from .tree import * |
@ -0,0 +1,743 @@ |
|||
""" |
|||
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, |
|||
Generic, |
|||
List, |
|||
Optional, |
|||
Set, |
|||
TYPE_CHECKING, |
|||
Tuple, |
|||
Type, |
|||
TypeVar, |
|||
Union, |
|||
) |
|||
from dataclasses import dataclass |
|||
from textwrap import TextWrapper |
|||
|
|||
import sys |
|||
import re |
|||
|
|||
from .enums import AppCommandOptionType, AppCommandType |
|||
from ..interactions import Interaction |
|||
from ..enums import ChannelType, try_enum |
|||
from .models import Choice |
|||
from .errors import CommandSignatureMismatch, CommandAlreadyRegistered |
|||
from ..utils import resolve_annotation, MISSING, is_inside_class |
|||
from ..user import User |
|||
from ..member import Member |
|||
from ..role import Role |
|||
from ..mixins import Hashable |
|||
from ..permissions import Permissions |
|||
|
|||
if TYPE_CHECKING: |
|||
from typing_extensions import ParamSpec, Concatenate |
|||
from ..interactions import Interaction |
|||
from ..types.interactions import ( |
|||
ResolvedData, |
|||
PartialThread, |
|||
PartialChannel, |
|||
ApplicationCommandInteractionDataOption, |
|||
) |
|||
from ..state import ConnectionState |
|||
from .namespace import Namespace |
|||
|
|||
__all__ = ( |
|||
'CommandParameter', |
|||
'Command', |
|||
'Group', |
|||
'command', |
|||
'describe', |
|||
) |
|||
|
|||
if TYPE_CHECKING: |
|||
P = ParamSpec('P') |
|||
else: |
|||
P = TypeVar('P') |
|||
|
|||
T = TypeVar('T') |
|||
GroupT = TypeVar('GroupT', bound='Group') |
|||
Coro = Coroutine[Any, Any, T] |
|||
|
|||
if TYPE_CHECKING: |
|||
CommandCallback = Union[ |
|||
Callable[Concatenate[GroupT, Interaction, P], Coro[T]], |
|||
Callable[Concatenate[Interaction, P], Coro[T]], |
|||
] |
|||
else: |
|||
CommandCallback = Callable[..., Coro[T]] |
|||
|
|||
|
|||
VALID_SLASH_COMMAND_NAME = re.compile(r'^[\w-]{1,32}$') |
|||
CAMEL_CASE_REGEX = re.compile(r'(?<!^)(?=[A-Z])') |
|||
|
|||
|
|||
def _shorten( |
|||
input: str, |
|||
*, |
|||
_wrapper: TextWrapper = TextWrapper(width=100, max_lines=1, replace_whitespace=True, placeholder='...'), |
|||
) -> str: |
|||
return _wrapper.fill(' '.join(input.strip().split())) |
|||
|
|||
|
|||
def _to_kebab_case(text: str) -> str: |
|||
return CAMEL_CASE_REGEX.sub('-', text).lower() |
|||
|
|||
|
|||
@dataclass |
|||
class CommandParameter: |
|||
"""Represents a application command parameter. |
|||
|
|||
Attributes |
|||
----------- |
|||
name: :class:`str` |
|||
The name of the parameter. |
|||
description: :class:`str` |
|||
The description of the parameter |
|||
required: :class:`bool` |
|||
Whether the parameter is required |
|||
choices: List[:class:`~discord.app_commands.Choice`] |
|||
A list of choices this parameter takes |
|||
type: :class:`~discord.app_commands.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[:class:`int`] |
|||
The minimum supported value for this parameter. |
|||
max_value: Optional[:class:`int`] |
|||
The maximum supported value for this parameter. |
|||
autocomplete: :class:`bool` |
|||
Whether this parameter enables autocomplete. |
|||
""" |
|||
|
|||
name: str = MISSING |
|||
description: str = MISSING |
|||
required: bool = MISSING |
|||
default: Any = MISSING |
|||
choices: List[Choice] = MISSING |
|||
type: AppCommandOptionType = MISSING |
|||
channel_types: List[ChannelType] = MISSING |
|||
min_value: Optional[int] = None |
|||
max_value: Optional[int] = None |
|||
autocomplete: bool = MISSING |
|||
annotation: Any = MISSING |
|||
# restrictor: Optional[RestrictorType] = None |
|||
|
|||
def to_dict(self) -> Dict[str, Any]: |
|||
base = { |
|||
'type': self.type.value, |
|||
'name': self.name, |
|||
'description': self.description, |
|||
'required': self.required, |
|||
} |
|||
|
|||
if self.choices: |
|||
base['choices'] = [choice.to_dict() for choice in self.choices] |
|||
if self.channel_types: |
|||
base['channel_types'] = [t.value for t in self.channel_types] |
|||
if self.autocomplete: |
|||
base['autocomplete'] = True |
|||
if self.min_value is not None: |
|||
base['min_value'] = self.min_value |
|||
if self.max_value is not None: |
|||
base['max_value'] = self.max_value |
|||
|
|||
return base |
|||
|
|||
|
|||
annotation_to_option_type: Dict[Any, AppCommandOptionType] = { |
|||
str: AppCommandOptionType.string, |
|||
int: AppCommandOptionType.integer, |
|||
float: AppCommandOptionType.number, |
|||
bool: AppCommandOptionType.boolean, |
|||
User: AppCommandOptionType.user, |
|||
Member: AppCommandOptionType.user, |
|||
Role: AppCommandOptionType.role, |
|||
# StageChannel: AppCommandOptionType.channel, |
|||
# StoreChannel: AppCommandOptionType.channel, |
|||
# VoiceChannel: AppCommandOptionType.channel, |
|||
# TextChannel: AppCommandOptionType.channel, |
|||
} |
|||
|
|||
NoneType = type(None) |
|||
allowed_default_types: Dict[AppCommandOptionType, Tuple[Type[Any], ...]] = { |
|||
AppCommandOptionType.string: (str, NoneType), |
|||
AppCommandOptionType.integer: (int, NoneType), |
|||
AppCommandOptionType.boolean: (bool, NoneType), |
|||
} |
|||
|
|||
|
|||
# Some sanity checks: |
|||
# str => string |
|||
# int => int |
|||
# User => user |
|||
# etc ... |
|||
# Optional[str] => string, required: false, default: None |
|||
# Optional[int] => integer, required: false, default: None |
|||
# Optional[Model] = None => resolved, required: false, default: None |
|||
# Optional[Model] can only have (CommandParameter, None) as default |
|||
# Optional[int | str | bool] can have (CommandParameter, None, int | str | bool) as a default |
|||
# Union[str, Member] => disallowed |
|||
# Union[int, str] => disallowed |
|||
# Union[Member, User] => user |
|||
# Optional[Union[Member, User]] => user, required: false, default: None |
|||
# Union[Member, User, Object] => mentionable |
|||
# Union[Models] => mentionable |
|||
# Optional[Union[Models]] => mentionable, required: false, default: None |
|||
|
|||
|
|||
def _annotation_to_type( |
|||
annotation: Any, |
|||
*, |
|||
mapping=annotation_to_option_type, |
|||
_none=NoneType, |
|||
) -> Tuple[AppCommandOptionType, Any]: |
|||
# Straight simple case, a regular ol' parameter |
|||
try: |
|||
option_type = mapping[annotation] |
|||
except KeyError: |
|||
pass |
|||
else: |
|||
return (option_type, MISSING) |
|||
|
|||
# Check if there's an origin |
|||
origin = getattr(annotation, '__origin__', None) |
|||
if origin is not Union: # TODO: Python 3.10 |
|||
# Only Union/Optional is supported so bail early |
|||
raise TypeError(f'unsupported type annotation {annotation!r}') |
|||
|
|||
default = MISSING |
|||
if annotation.__args__[-1] is _none: |
|||
if len(annotation.__args__) == 2: |
|||
underlying = annotation.__args__[0] |
|||
option_type = mapping.get(underlying) |
|||
if option_type is None: |
|||
raise TypeError(f'unsupported inner optional type {underlying!r}') |
|||
return (option_type, None) |
|||
else: |
|||
args = annotation.__args__[:-1] |
|||
default = None |
|||
else: |
|||
args = annotation.__args__ |
|||
|
|||
# At this point only models are allowed |
|||
# Since Optional[int | bool | str] will be taken care of above |
|||
# The only valid transformations here are: |
|||
# [Member, User] => user |
|||
# [Member, User, Role] => mentionable |
|||
# [Member | User, Role] => mentionable |
|||
supported_types: Set[Any] = {Role, Member, User} |
|||
if not all(arg in supported_types for arg in args): |
|||
raise TypeError(f'unsupported types given inside {annotation!r}') |
|||
if args == (User, Member) or args == (Member, User): |
|||
return (AppCommandOptionType.user, default) |
|||
|
|||
return (AppCommandOptionType.mentionable, default) |
|||
|
|||
|
|||
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): |
|||
raise TypeError('description must be a string') |
|||
|
|||
param.description = description |
|||
|
|||
if descriptions: |
|||
first = next(iter(descriptions)) |
|||
raise TypeError(f'unknown parameter given: {first}') |
|||
|
|||
|
|||
def _get_parameter(annotation: Any, parameter: inspect.Parameter) -> CommandParameter: |
|||
(type, default) = _annotation_to_type(annotation) |
|||
if default is MISSING: |
|||
default = parameter.default |
|||
if default is parameter.empty: |
|||
default = MISSING |
|||
|
|||
result = CommandParameter( |
|||
type=type, |
|||
default=default, |
|||
required=default is MISSING, |
|||
name=parameter.name, |
|||
) |
|||
|
|||
if parameter.kind in (parameter.POSITIONAL_ONLY, parameter.VAR_KEYWORD, parameter.VAR_POSITIONAL): |
|||
raise TypeError(f'unsupported parameter kind in callback: {parameter.kind!s}') |
|||
|
|||
# Verify validity of the default parameter |
|||
if result.default is not MISSING: |
|||
valid_types: Tuple[Any, ...] = allowed_default_types.get(result.type, (NoneType,)) |
|||
if not isinstance(result.default, valid_types): |
|||
raise TypeError(f'invalid default parameter type given ({result.default.__class__}), expected {valid_types}') |
|||
|
|||
result.annotation = annotation |
|||
return result |
|||
|
|||
|
|||
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 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'annotation for {parameter.name} must be given') |
|||
|
|||
resolved = resolve_annotation(parameter.annotation, globalns, globalns, cache) |
|||
param = _get_parameter(resolved, parameter) |
|||
parameters.append(param) |
|||
|
|||
values = sorted(parameters, key=lambda a: a.required, reverse=True) |
|||
result = {v.name: v for v in values} |
|||
|
|||
try: |
|||
descriptions = func.__discord_app_commands_param_description__ |
|||
except AttributeError: |
|||
pass |
|||
else: |
|||
_populate_descriptions(result, descriptions) |
|||
|
|||
return result |
|||
|
|||
|
|||
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>` |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
Attributes |
|||
------------ |
|||
name: :class:`str` |
|||
The name of the application command. |
|||
type: :class:`AppCommandType` |
|||
The type of application command. |
|||
callback: :ref:`coroutine <coroutine>` |
|||
The coroutine that is executed when the command is called. |
|||
description: :class:`str` |
|||
The description of the application command. This shows up in the UI to describe |
|||
the application command. |
|||
parent: Optional[:class:`CommandGroup`] |
|||
The parent application command. ``None`` if there isn't one. |
|||
""" |
|||
|
|||
def __init__( |
|||
self, |
|||
*, |
|||
name: str, |
|||
description: str, |
|||
callback: CommandCallback[GroupT, P, T], |
|||
type: AppCommandType = AppCommandType.chat_input, |
|||
parent: Optional[Group] = None, |
|||
): |
|||
self.name: str = name |
|||
self.description: str = description |
|||
self._callback: CommandCallback[GroupT, P, T] = callback |
|||
self.parent: Optional[Group] = parent |
|||
self.binding: Optional[GroupT] = None |
|||
self.type: AppCommandType = type |
|||
self._params: Dict[str, CommandParameter] = _extract_parameters_from_callback(callback, callback.__globals__) |
|||
|
|||
def _copy_with_binding(self, binding: GroupT) -> Command: |
|||
cls = self.__class__ |
|||
copy = cls.__new__(cls) |
|||
copy.name = self.name |
|||
copy.description = self.description |
|||
copy._callback = self._callback |
|||
copy.parent = self.parent |
|||
copy.type = self.type |
|||
copy._params = self._params.copy() |
|||
copy.binding = binding |
|||
return copy |
|||
|
|||
def to_dict(self) -> 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 = self.type.value if self.parent is None else AppCommandOptionType.subcommand.value |
|||
return { |
|||
'name': self.name, |
|||
'description': self.description, |
|||
'type': option_type, |
|||
'options': [param.to_dict() for param in self._params.values()], |
|||
} |
|||
|
|||
async def _invoke_with_namespace(self, interaction: Interaction, namespace: Namespace) -> T: |
|||
defaults = ((name, param.default) for name, param in self._params.items() if not param.required) |
|||
namespace._update_with_defaults(defaults) |
|||
# 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, **namespace.__dict__) # type: ignore |
|||
return await self._callback(interaction, **namespace.__dict__) # type: ignore |
|||
except TypeError: |
|||
# 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 |
|||
|
|||
def get_parameter(self, name: str) -> Optional[CommandParameter]: |
|||
"""Returns the :class:`CommandParameter` with the given name. |
|||
|
|||
Parameters |
|||
----------- |
|||
name: :class:`str` |
|||
The parameter name to get. |
|||
|
|||
Returns |
|||
-------- |
|||
Optional[:class:`CommandParameter`] |
|||
The command parameter, if found. |
|||
""" |
|||
return self._params.get(name) |
|||
|
|||
@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 |
|||
|
|||
def _get_internal_command(self, name: str) -> Optional[Union[Command, Group]]: |
|||
return None |
|||
|
|||
|
|||
class Group: |
|||
"""A class that implements an application command group. |
|||
|
|||
These are usually inherited rather than created manually. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
Attributes |
|||
------------ |
|||
name: :class:`str` |
|||
The name of the group. If not given, it defaults to a lower-case |
|||
kebab-case version of the class name. |
|||
description: :class:`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. |
|||
parent: Optional[:class:`CommandGroup`] |
|||
The parent group. ``None`` if there isn't one. |
|||
""" |
|||
|
|||
__discord_app_commands_group_children__: ClassVar[List[Union[Command, Group]]] = [] |
|||
__discord_app_commands_group_name__: str = MISSING |
|||
__discord_app_commands_group_description__: str = MISSING |
|||
|
|||
def __init_subclass__(cls, *, name: str = MISSING, description: str = MISSING) -> None: |
|||
cls.__discord_app_commands_group_children__ = children = [ |
|||
member for member in cls.__dict__.values() if isinstance(member, (Group, Command)) and member.parent is None |
|||
] |
|||
|
|||
found = set() |
|||
for child in children: |
|||
if child.name in found: |
|||
raise TypeError(f'Command {child.name} is a duplicate') |
|||
found.add(child.name) |
|||
|
|||
if name is MISSING: |
|||
cls.__discord_app_commands_group_name__ = _to_kebab_case(cls.__name__) |
|||
else: |
|||
cls.__discord_app_commands_group_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__) |
|||
else: |
|||
cls.__discord_app_commands_group_description__ = description |
|||
|
|||
if len(children) > 25: |
|||
raise TypeError('groups cannot have more than 25 commands') |
|||
|
|||
def __init__( |
|||
self, |
|||
*, |
|||
name: str = MISSING, |
|||
description: str = MISSING, |
|||
parent: Optional[Group] = None, |
|||
): |
|||
cls = self.__class__ |
|||
self.name: str = name if name is not MISSING else cls.__discord_app_commands_group_name__ |
|||
self.description: str = description or cls.__discord_app_commands_group_description__ |
|||
|
|||
if not self.description: |
|||
raise TypeError('groups must have a description') |
|||
|
|||
self.parent: Optional[Group] = parent |
|||
|
|||
self._children: Dict[str, Union[Command, Group]] = { |
|||
child.name: child._copy_with_binding(self) for child in self.__discord_app_commands_group_children__ |
|||
} |
|||
|
|||
for child in self._children.values(): |
|||
child.parent = self |
|||
|
|||
if parent is not None and parent.parent is not None: |
|||
raise ValueError('groups can only be nested at most one level') |
|||
|
|||
def _copy_with_binding(self, binding: Group) -> Group: |
|||
cls = self.__class__ |
|||
copy = cls.__new__(cls) |
|||
copy.name = self.name |
|||
copy.description = self.description |
|||
copy.parent = self.parent |
|||
copy._children = {child.name: child._copy_with_binding(binding) for child in self._children.values()} |
|||
return copy |
|||
|
|||
def to_dict(self) -> 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 |
|||
return { |
|||
'name': self.name, |
|||
'description': self.description, |
|||
'type': option_type, |
|||
'options': [child.to_dict() for child in self._children.values()], |
|||
} |
|||
|
|||
@property |
|||
def root_parent(self) -> Optional[Group]: |
|||
"""Optional[:class:`Group`]: The parent of this group.""" |
|||
return self.parent |
|||
|
|||
def _get_internal_command(self, name: str) -> Optional[Union[Command, Group]]: |
|||
return self._children.get(name) |
|||
|
|||
def add_command(self, command: Union[Command, Group], /, *, override: bool = False): |
|||
"""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. |
|||
""" |
|||
|
|||
if not override and command.name in self._children: |
|||
raise CommandAlreadyRegistered(command.name, guild_id=None) |
|||
|
|||
self._children[command.name] = command |
|||
if len(self._children) > 25: |
|||
raise ValueError('maximum number of child commands exceeded') |
|||
|
|||
def remove_command(self, name: str, /) -> Optional[Union[Command, Group]]: |
|||
"""Remove 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, 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: str = MISSING, |
|||
description: str = MISSING, |
|||
) -> Callable[[CommandCallback[GroupT, P, T]], Command[GroupT, P, T]]: |
|||
"""Creates an application command under this group. |
|||
|
|||
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. |
|||
""" |
|||
|
|||
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, |
|||
type=AppCommandType.chat_input, |
|||
parent=self, |
|||
) |
|||
self.add_command(command) |
|||
return command |
|||
|
|||
return decorator |
|||
|
|||
|
|||
def command( |
|||
*, |
|||
name: str = MISSING, |
|||
description: str = 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. |
|||
""" |
|||
|
|||
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, |
|||
type=AppCommandType.chat_input, |
|||
parent=None, |
|||
) |
|||
|
|||
return decorator |
|||
|
|||
|
|||
def describe(**parameters: 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() |
|||
@app_commads.describe(member='the member to ban') |
|||
async def ban(interaction: discord.Interaction, member: discord.Member): |
|||
await interaction.response.send_message(f'Banned {member}') |
|||
|
|||
Parameters |
|||
----------- |
|||
\*\*parameters |
|||
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: |
|||
inner.__discord_app_commands_param_description__ = parameters # type: ignore - Runtime attribute assignment |
|||
|
|||
return inner |
|||
|
|||
return decorator |
@ -0,0 +1,53 @@ |
|||
""" |
|||
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 ..enums import Enum |
|||
|
|||
__all__ = ( |
|||
'AppCommandOptionType', |
|||
'AppCommandType', |
|||
) |
|||
|
|||
|
|||
class AppCommandOptionType(Enum): |
|||
subcommand = 1 |
|||
subcommand_group = 2 |
|||
string = 3 |
|||
integer = 4 |
|||
boolean = 5 |
|||
user = 6 |
|||
channel = 7 |
|||
role = 8 |
|||
mentionable = 9 |
|||
number = 10 |
|||
attachment = 11 |
|||
|
|||
def is_argument(self) -> bool: |
|||
return 11 >= self.value >= 3 |
|||
|
|||
|
|||
class AppCommandType(Enum): |
|||
chat_input = 1 |
|||
user = 2 |
|||
message = 3 |
@ -0,0 +1,96 @@ |
|||
""" |
|||
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 |
|||
|
|||
from typing import TYPE_CHECKING, List, Optional, Union |
|||
from ..errors import DiscordException |
|||
|
|||
__all__ = ( |
|||
'CommandAlreadyRegistered', |
|||
'CommandSignatureMismatch', |
|||
'CommandNotFound', |
|||
) |
|||
|
|||
if TYPE_CHECKING: |
|||
from .commands import Command, Group |
|||
|
|||
|
|||
class CommandAlreadyRegistered(DiscordException): |
|||
"""An exception raised when a command is already registered. |
|||
|
|||
Attributes |
|||
----------- |
|||
name: :class:`str` |
|||
The name of the command already registered. |
|||
guild_id: Optional[:class:`int`] |
|||
The guild ID this command was already registered at. |
|||
If ``None`` then it was a global command. |
|||
""" |
|||
|
|||
def __init__(self, name: str, guild_id: Optional[int]): |
|||
self.name = name |
|||
self.guild_id = guild_id |
|||
super().__init__(f'Command {name!r} already registered.') |
|||
|
|||
|
|||
class CommandNotFound(DiscordException): |
|||
"""An exception raised when an application command could not be found. |
|||
|
|||
Attributes |
|||
------------ |
|||
name: :class:`str` |
|||
The name of the application command not found. |
|||
parents: List[:class:`str`] |
|||
A list of parent command names that were previously found |
|||
prior to the application command not being found. |
|||
""" |
|||
|
|||
def __init__(self, name: str, parents: List[str]): |
|||
self.name = name |
|||
self.parents = parents |
|||
super().__init__(f'Application command {name!r} not found') |
|||
|
|||
|
|||
class CommandSignatureMismatch(DiscordException): |
|||
"""An exception raised when an application command from Discord has a different signature |
|||
from the one provided in the code. This happens because your command definition differs |
|||
from the command definition you provided Discord. Either your code is out of date or the |
|||
data from Discord is out of sync. |
|||
|
|||
Attributes |
|||
------------ |
|||
command: Union[:class:`~discord.app_commands.Command`, :class:`~discord.app_commands.Group`] |
|||
The command that had the signature mismatch. |
|||
""" |
|||
|
|||
def __init__(self, command: Union[Command, Group]): |
|||
self.command: Union[Command, Group] = command |
|||
msg = ( |
|||
f'The signature for command {command!r} is different from the one provided by Discord. ' |
|||
'This can happen because either your code is out of date or you have not synced the ' |
|||
'commands with Discord, causing the mismatch in data. It is recommended to sync the ' |
|||
'command tree to fix this issue.' |
|||
) |
|||
super().__init__(msg) |
@ -0,0 +1,592 @@ |
|||
""" |
|||
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 |
|||
from datetime import datetime |
|||
|
|||
|
|||
from ..permissions import Permissions |
|||
from ..enums import ChannelType, try_enum |
|||
from ..mixins import Hashable |
|||
from ..utils import _get_as_snowflake, parse_time, snowflake_time |
|||
from .enums import AppCommandOptionType, AppCommandType |
|||
from typing import List, NamedTuple, TYPE_CHECKING, Optional, Union |
|||
|
|||
__all__ = ( |
|||
'AppCommand', |
|||
'AppCommandGroup', |
|||
'AppCommandChannel', |
|||
'AppCommandThread', |
|||
'Argument', |
|||
'Choice', |
|||
) |
|||
|
|||
|
|||
def is_app_command_argument_type(value: int) -> bool: |
|||
return 11 >= value >= 3 |
|||
|
|||
|
|||
if TYPE_CHECKING: |
|||
from ..types.command import ( |
|||
ApplicationCommand as ApplicationCommandPayload, |
|||
ApplicationCommandOptionChoice, |
|||
ApplicationCommandOption, |
|||
) |
|||
from ..types.interactions import ( |
|||
PartialChannel, |
|||
PartialThread, |
|||
) |
|||
from ..types.threads import ThreadMetadata |
|||
from ..state import ConnectionState |
|||
from ..guild import GuildChannel, Guild |
|||
from ..channel import TextChannel |
|||
from ..threads import Thread |
|||
|
|||
ApplicationCommandParent = Union['AppCommand', 'AppCommandGroup'] |
|||
|
|||
|
|||
class AppCommand(Hashable): |
|||
"""Represents a application command. |
|||
|
|||
In common parlance this is referred to as a "Slash Command" or a |
|||
"Context Menu Command". |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
.. container:: operations |
|||
|
|||
.. describe:: x == y |
|||
|
|||
Checks if two application commands are equal. |
|||
|
|||
.. describe:: x != y |
|||
|
|||
Checks if two application commands are not equal. |
|||
|
|||
.. describe:: hash(x) |
|||
|
|||
Returns the application command's hash. |
|||
|
|||
.. describe:: str(x) |
|||
|
|||
Returns the application command's name. |
|||
|
|||
Attributes |
|||
----------- |
|||
id: :class:`int` |
|||
The application command's ID. |
|||
application_id: :class:`int` |
|||
The application command's application's ID. |
|||
type: :class:`ApplicationCommandType` |
|||
The application command's type. |
|||
name: :class:`str` |
|||
The application command's name. |
|||
description: :class:`str` |
|||
The application command's description. |
|||
""" |
|||
|
|||
__slots__ = ( |
|||
'id', |
|||
'type', |
|||
'application_id', |
|||
'name', |
|||
'description', |
|||
'options', |
|||
'_state', |
|||
) |
|||
|
|||
def __init__(self, *, data: ApplicationCommandPayload, state=None): |
|||
self._state = state |
|||
self._from_data(data) |
|||
|
|||
def _from_data(self, data: ApplicationCommandPayload): |
|||
self.id: int = int(data['id']) |
|||
self.application_id: int = int(data['application_id']) |
|||
self.name: str = data['name'] |
|||
self.description: str = data['description'] |
|||
self.type: AppCommandType = try_enum(AppCommandType, data.get('type', 1)) |
|||
self.options = [app_command_option_factory(data=d, parent=self, state=self._state) for d in data.get('options', [])] |
|||
|
|||
def to_dict(self) -> ApplicationCommandPayload: |
|||
return { |
|||
'id': self.id, |
|||
'type': self.type.value, |
|||
'application_id': self.application_id, |
|||
'name': self.name, |
|||
'description': self.description, |
|||
'options': [opt.to_dict() for opt in self.options], |
|||
} # type: ignore -- Type checker does not understand this literal. |
|||
|
|||
def __str__(self) -> str: |
|||
return self.name |
|||
|
|||
def __repr__(self) -> str: |
|||
return f'<{self.__class__.__name__} id={self.id!r} name={self.name!r} type={self.type!r}>' |
|||
|
|||
|
|||
class Choice(NamedTuple): |
|||
"""Represents an application command argument choice. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
.. container:: operations |
|||
|
|||
.. describe:: x == y |
|||
|
|||
Checks if two choices are equal. |
|||
|
|||
.. describe:: x != y |
|||
|
|||
Checks if two choices are not equal. |
|||
|
|||
Parameters |
|||
----------- |
|||
name: :class:`str` |
|||
The name of the choice. Used for display purposes. |
|||
value: Union[:class:`int`, :class:`str`, :class:`float`] |
|||
The value of the choice. |
|||
""" |
|||
|
|||
name: str |
|||
value: Union[int, str, float] |
|||
|
|||
def to_dict(self) -> ApplicationCommandOptionChoice: |
|||
return { |
|||
'name': self.name, |
|||
'value': self.value, |
|||
} # type: ignore -- Type checker does not understand this literal. |
|||
|
|||
|
|||
class AppCommandChannel(Hashable): |
|||
"""Represents an application command partially resolved channel object. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
.. container:: operations |
|||
|
|||
.. describe:: x == y |
|||
|
|||
Checks if two channels are equal. |
|||
|
|||
.. describe:: x != y |
|||
|
|||
Checks if two channels are not equal. |
|||
|
|||
.. describe:: hash(x) |
|||
|
|||
Returns the channel's hash. |
|||
|
|||
.. describe:: str(x) |
|||
|
|||
Returns the channel's name. |
|||
|
|||
Attributes |
|||
----------- |
|||
id: :class:`int` |
|||
The ID of the channel. |
|||
type: :class:`~discord.ChannelType` |
|||
The type of channel. |
|||
name: :class:`str` |
|||
The name of the channel. |
|||
permissions: :class:`~discord.Permissions` |
|||
The resolved permissions of the user who invoked |
|||
the application command in that channel. |
|||
guild_id: :class:`int` |
|||
The guild ID this channel belongs to. |
|||
""" |
|||
|
|||
__slots__ = ( |
|||
'id', |
|||
'type', |
|||
'name', |
|||
'permissions', |
|||
'guild_id', |
|||
'_state', |
|||
) |
|||
|
|||
def __init__( |
|||
self, |
|||
*, |
|||
state: ConnectionState, |
|||
data: PartialChannel, |
|||
guild_id: int, |
|||
): |
|||
self._state = state |
|||
self.guild_id = guild_id |
|||
self.id = int(data['id']) |
|||
self.type = try_enum(ChannelType, data['type']) |
|||
self.name = data['name'] |
|||
self.permissions = Permissions(int(data['permissions'])) |
|||
|
|||
def __str__(self) -> str: |
|||
return self.name |
|||
|
|||
def __repr__(self) -> str: |
|||
return f'<{self.__class__.__name__} id={self.id!r} name={self.name!r} type={self.type!r}>' |
|||
|
|||
@property |
|||
def guild(self) -> Optional[Guild]: |
|||
"""Optional[:class:`~discord.Guild`]: The channel's guild, from cache, if found.""" |
|||
return self._state._get_guild(self.guild_id) |
|||
|
|||
def resolve(self) -> Optional[GuildChannel]: |
|||
"""Resolves the application command channel to the appropriate channel |
|||
from cache if found. |
|||
|
|||
Returns |
|||
-------- |
|||
Optional[:class:`.abc.GuildChannel`] |
|||
The resolved guild channel or ``None`` if not found in cache. |
|||
""" |
|||
guild = self._state._get_guild(self.guild_id) |
|||
if guild is not None: |
|||
return guild.get_channel(self.id) |
|||
return None |
|||
|
|||
async def fetch(self) -> GuildChannel: |
|||
"""|coro| |
|||
|
|||
Fetches the partial channel to a full :class:`.abc.GuildChannel`. |
|||
|
|||
Raises |
|||
-------- |
|||
NotFound |
|||
The channel was not found. |
|||
Forbidden |
|||
You do not have the permissions required to get a channel. |
|||
HTTPException |
|||
Retrieving the channel failed. |
|||
|
|||
Returns |
|||
-------- |
|||
:class:`.abc.GuildChannel` |
|||
The full channel. |
|||
""" |
|||
client = self._state._get_client() |
|||
return await client.fetch_channel(self.id) # type: ignore -- This is explicit narrowing |
|||
|
|||
@property |
|||
def mention(self) -> str: |
|||
""":class:`str`: The string that allows you to mention the channel.""" |
|||
return f'<#{self.id}>' |
|||
|
|||
@property |
|||
def created_at(self) -> datetime: |
|||
""":class:`datetime.datetime`: An aware timestamp of when this channel was created in UTC.""" |
|||
return snowflake_time(self.id) |
|||
|
|||
|
|||
class AppCommandThread(Hashable): |
|||
"""Represents an application command partially resolved thread object. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
.. container:: operations |
|||
|
|||
.. describe:: x == y |
|||
|
|||
Checks if two thread are equal. |
|||
|
|||
.. describe:: x != y |
|||
|
|||
Checks if two thread are not equal. |
|||
|
|||
.. describe:: hash(x) |
|||
|
|||
Returns the thread's hash. |
|||
|
|||
.. describe:: str(x) |
|||
|
|||
Returns the thread's name. |
|||
|
|||
Attributes |
|||
----------- |
|||
id: :class:`int` |
|||
The ID of the thread. |
|||
type: :class:`~discord.ChannelType` |
|||
The type of thread. |
|||
name: :class:`str` |
|||
The name of the thread. |
|||
parent_id: :class:`int` |
|||
The parent text channel ID this thread belongs to. |
|||
permissions: :class:`~discord.Permissions` |
|||
The resolved permissions of the user who invoked |
|||
the application command in that thread. |
|||
guild_id: :class:`int` |
|||
The guild ID this thread belongs to. |
|||
archived: :class:`bool` |
|||
Whether the thread is archived. |
|||
locked: :class:`bool` |
|||
Whether the thread is locked. |
|||
invitable: :class:`bool` |
|||
Whether non-moderators can add other non-moderators to this thread. |
|||
This is always ``True`` for public threads. |
|||
archiver_id: Optional[:class:`int`] |
|||
The user's ID that archived this thread. |
|||
auto_archive_duration: :class:`int` |
|||
The duration in minutes until the thread is automatically archived due to inactivity. |
|||
Usually a value of 60, 1440, 4320 and 10080. |
|||
archive_timestamp: :class:`datetime.datetime` |
|||
An aware timestamp of when the thread's archived status was last updated in UTC. |
|||
""" |
|||
|
|||
__slots__ = ( |
|||
'id', |
|||
'type', |
|||
'name', |
|||
'permissions', |
|||
'guild_id', |
|||
'parent_id', |
|||
'archived', |
|||
'archiver_id', |
|||
'auto_archive_duration', |
|||
'archive_timestamp', |
|||
'locked', |
|||
'invitable', |
|||
'_created_at', |
|||
'_state', |
|||
) |
|||
|
|||
def __init__( |
|||
self, |
|||
*, |
|||
state: ConnectionState, |
|||
data: PartialThread, |
|||
guild_id: int, |
|||
): |
|||
self._state = state |
|||
self.guild_id = guild_id |
|||
self.id = int(data['id']) |
|||
self.parent_id = int(data['parent_id']) |
|||
self.type = try_enum(ChannelType, data['type']) |
|||
self.name = data['name'] |
|||
self.permissions = Permissions(int(data['permissions'])) |
|||
self._unroll_metadata(data['thread_metadata']) |
|||
|
|||
def __str__(self) -> str: |
|||
return self.name |
|||
|
|||
def __repr__(self) -> str: |
|||
return f'<{self.__class__.__name__} id={self.id!r} name={self.name!r} archived={self.archived} type={self.type!r}>' |
|||
|
|||
@property |
|||
def guild(self) -> Optional[Guild]: |
|||
"""Optional[:class:`~discord.Guild`]: The channel's guild, from cache, if found.""" |
|||
return self._state._get_guild(self.guild_id) |
|||
|
|||
def _unroll_metadata(self, data: ThreadMetadata): |
|||
self.archived = data['archived'] |
|||
self.archiver_id = _get_as_snowflake(data, 'archiver_id') |
|||
self.auto_archive_duration = data['auto_archive_duration'] |
|||
self.archive_timestamp = parse_time(data['archive_timestamp']) |
|||
self.locked = data.get('locked', False) |
|||
self.invitable = data.get('invitable', True) |
|||
self._created_at = parse_time(data.get('create_timestamp')) |
|||
|
|||
@property |
|||
def parent(self) -> Optional[TextChannel]: |
|||
"""Optional[:class:`TextChannel`]: The parent channel this thread belongs to.""" |
|||
return self.guild.get_channel(self.parent_id) # type: ignore |
|||
|
|||
@property |
|||
def mention(self) -> str: |
|||
""":class:`str`: The string that allows you to mention the thread.""" |
|||
return f'<#{self.id}>' |
|||
|
|||
@property |
|||
def created_at(self) -> Optional[datetime]: |
|||
"""An aware timestamp of when the thread was created in UTC. |
|||
|
|||
.. note:: |
|||
|
|||
This timestamp only exists for threads created after 9 January 2022, otherwise returns ``None``. |
|||
""" |
|||
return self._created_at |
|||
|
|||
def resolve(self) -> Optional[Thread]: |
|||
"""Resolves the application command channel to the appropriate channel |
|||
from cache if found. |
|||
|
|||
Returns |
|||
-------- |
|||
Optional[:class:`.abc.GuildChannel`] |
|||
The resolved guild channel or ``None`` if not found in cache. |
|||
""" |
|||
guild = self._state._get_guild(self.guild_id) |
|||
if guild is not None: |
|||
return guild.get_thread(self.id) |
|||
return None |
|||
|
|||
async def fetch(self) -> Thread: |
|||
"""|coro| |
|||
|
|||
Fetches the partial channel to a full :class:`~discord.Thread`. |
|||
|
|||
Raises |
|||
-------- |
|||
NotFound |
|||
The thread was not found. |
|||
Forbidden |
|||
You do not have the permissions required to get a thread. |
|||
HTTPException |
|||
Retrieving the thread failed. |
|||
|
|||
Returns |
|||
-------- |
|||
:class:`~discord.Thread` |
|||
The full thread. |
|||
""" |
|||
client = self._state._get_client() |
|||
return await client.fetch_channel(self.id) # type: ignore -- This is explicit narrowing |
|||
|
|||
|
|||
class Argument: |
|||
"""Represents a application command argument. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
Attributes |
|||
------------ |
|||
type: :class:`AppCommandOptionType` |
|||
The type of argument. |
|||
name: :class:`str` |
|||
The name of the argument. |
|||
description: :class:`str` |
|||
The description of the argument. |
|||
required: :class:`bool` |
|||
Whether the argument is required. |
|||
choices: List[:class:`Choice`] |
|||
A list of choices for the command to choose from for this argument. |
|||
parent: Union[:class:`AppCommand`, :class:`AppCommandGroup`] |
|||
The parent application command that has this argument. |
|||
""" |
|||
|
|||
__slots__ = ( |
|||
'type', |
|||
'name', |
|||
'description', |
|||
'required', |
|||
'choices', |
|||
'parent', |
|||
'_state', |
|||
) |
|||
|
|||
def __init__(self, *, parent: ApplicationCommandParent, data: ApplicationCommandOption, state=None): |
|||
self._state = state |
|||
self.parent = parent |
|||
self._from_data(data) |
|||
|
|||
def __repr__(self) -> str: |
|||
return f'<{self.__class__.__name__} name={self.name!r} type={self.type!r} required={self.required}>' |
|||
|
|||
def _from_data(self, data: ApplicationCommandOption): |
|||
self.type: AppCommandOptionType = try_enum(AppCommandOptionType, data['type']) |
|||
self.name: str = data['name'] |
|||
self.description: str = data['description'] |
|||
self.required: bool = data.get('required', False) |
|||
self.choices: List[Choice] = [Choice(name=d['name'], value=d['value']) for d in data.get('choices', [])] |
|||
|
|||
def to_dict(self) -> ApplicationCommandOption: |
|||
return { |
|||
'name': self.name, |
|||
'type': self.type.value, |
|||
'description': self.description, |
|||
'required': self.required, |
|||
'choices': [choice.to_dict() for choice in self.choices], |
|||
'options': [], |
|||
} # type: ignore -- Type checker does not understand this literal. |
|||
|
|||
|
|||
class AppCommandGroup: |
|||
"""Represents a application command subcommand. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
Attributes |
|||
------------ |
|||
type: :class:`ApplicationCommandOptionType` |
|||
The type of subcommand. |
|||
name: :class:`str` |
|||
The name of the subcommand. |
|||
description: :class:`str` |
|||
The description of the subcommand. |
|||
required: :class:`bool` |
|||
Whether the subcommand is required. |
|||
choices: List[:class:`Choice`] |
|||
A list of choices for the command to choose from for this subcommand. |
|||
arguments: List[:class:`Argument`] |
|||
A list of arguments. |
|||
parent: Union[:class:`AppCommand`, :class:`AppCommandGroup`] |
|||
The parent application command. |
|||
""" |
|||
|
|||
__slots__ = ( |
|||
'type', |
|||
'name', |
|||
'description', |
|||
'required', |
|||
'choices', |
|||
'arguments', |
|||
'parent', |
|||
'_state', |
|||
) |
|||
|
|||
def __init__(self, *, parent: ApplicationCommandParent, data: ApplicationCommandOption, state=None): |
|||
self.parent = parent |
|||
self._state = state |
|||
self._from_data(data) |
|||
|
|||
def __repr__(self) -> str: |
|||
return f'<{self.__class__.__name__} name={self.name!r} type={self.type!r} required={self.required}>' |
|||
|
|||
def _from_data(self, data: ApplicationCommandOption): |
|||
self.type: AppCommandOptionType = try_enum(AppCommandOptionType, data['type']) |
|||
self.name: str = data['name'] |
|||
self.description: str = data['description'] |
|||
self.required: bool = data.get('required', False) |
|||
self.choices: List[Choice] = [Choice(name=d['name'], value=d['value']) for d in data.get('choices', [])] |
|||
self.arguments: List[Argument] = [ |
|||
Argument(parent=self, state=self._state, data=d) |
|||
for d in data.get('options', []) |
|||
if is_app_command_argument_type(d['type']) |
|||
] |
|||
|
|||
def to_dict(self) -> 'ApplicationCommandOption': |
|||
return { |
|||
'name': self.name, |
|||
'type': self.type.value, |
|||
'description': self.description, |
|||
'required': self.required, |
|||
'choices': [choice.to_dict() for choice in self.choices], |
|||
'options': [arg.to_dict() for arg in self.arguments], |
|||
} # type: ignore -- Type checker does not understand this literal. |
|||
|
|||
|
|||
def app_command_option_factory( |
|||
parent: ApplicationCommandParent, data: ApplicationCommandOption, *, state=None |
|||
) -> Union[Argument, AppCommandGroup]: |
|||
if is_app_command_argument_type(data['type']): |
|||
return Argument(parent=parent, data=data, state=state) |
|||
else: |
|||
return AppCommandGroup(parent=parent, data=data, state=state) |
@ -0,0 +1,160 @@ |
|||
""" |
|||
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 |
|||
|
|||
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Tuple |
|||
from ..interactions import Interaction |
|||
from ..member import Member |
|||
from ..object import Object |
|||
from ..role import Role |
|||
from ..message import Message, Attachment |
|||
from .models import AppCommandChannel, AppCommandThread |
|||
|
|||
if TYPE_CHECKING: |
|||
from ..types.interactions import ResolvedData, ApplicationCommandInteractionDataOption |
|||
|
|||
|
|||
class Namespace: |
|||
"""An object that holds the parameters being passed to a command in a mostly raw state. |
|||
|
|||
This class is deliberately simple and just holds the option name and resolved value as a simple |
|||
key-pair mapping. These attributes can be accessed using dot notation. For example, an option |
|||
with the name of ``example`` can be accessed using ``ns.example``. |
|||
|
|||
.. versionadded:: 2.0 |
|||
|
|||
.. container:: operations |
|||
|
|||
.. describe:: x == y |
|||
|
|||
Checks if two namespaces are equal by checking if all attributes are equal. |
|||
.. describe:: x != y |
|||
|
|||
Checks if two namespaces are not equal. |
|||
|
|||
This namespace object converts resolved objects into their appropriate form depending on their |
|||
type. Consult the table below for conversion information. |
|||
|
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
| Option Type | Resolved Type | |
|||
+==========================================+===============================================================================+ |
|||
| :attr:`AppCommandOptionType.string` | :class:`str` | |
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
| :attr:`AppCommandOptionType.integer` | :class:`int` | |
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
| :attr:`AppCommandOptionType.boolean` | :class:`bool` | |
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
| :attr:`AppCommandOptionType.number` | :class:`float` | |
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
| :attr:`AppCommandOptionType.user` | :class:`~discord.User` or :class:`~discord.Member` | |
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
| :attr:`AppCommandOptionType.channel` | :class:`.AppCommandChannel` or :class:`.AppCommandThread` | |
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
| :attr:`AppCommandOptionType.role` | :class:`~discord.Role` | |
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
| :attr:`AppCommandOptionType.mentionable` | :class:`~discord.User` or :class:`~discord.Member`, or :class:`~discord.Role` | |
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
| :attr:`AppCommandOptionType.attachment` | :class:`~discord.Attachment` | |
|||
+------------------------------------------+-------------------------------------------------------------------------------+ |
|||
""" |
|||
|
|||
def __init__( |
|||
self, |
|||
interaction: Interaction, |
|||
resolved: ResolvedData, |
|||
options: List[ApplicationCommandInteractionDataOption], |
|||
): |
|||
completed: Dict[str, Any] = {} |
|||
state = interaction._state |
|||
members = resolved.get('members', {}) |
|||
guild_id = interaction.guild_id |
|||
guild = (state._get_guild(guild_id) or Object(id=guild_id)) if guild_id is not None else None |
|||
for (user_id, user_data) in resolved.get('users', {}).items(): |
|||
try: |
|||
member_data = members[user_id] |
|||
except KeyError: |
|||
completed[user_id] = state.create_user(user_data) |
|||
else: |
|||
member_data['user'] = user_data |
|||
# Guild ID can't be None in this case. |
|||
# There's a type mismatch here that I don't actually care about |
|||
member = Member(state=state, guild=guild, data=member_data) # type: ignore |
|||
completed[user_id] = member |
|||
|
|||
completed.update( |
|||
{ |
|||
# The guild ID can't be None in this case. |
|||
role_id: Role(guild=guild, state=state, data=role_data) # type: ignore |
|||
for role_id, role_data in resolved.get('roles', {}).items() |
|||
} |
|||
) |
|||
|
|||
for (channel_id, channel_data) in resolved.get('channels', {}).items(): |
|||
if channel_data['type'] in (10, 11, 12): |
|||
# The guild ID can't be none in this case |
|||
completed[channel_id] = AppCommandThread(state=state, data=channel_data, guild_id=guild_id) # type: ignore |
|||
else: |
|||
# The guild ID can't be none in this case |
|||
completed[channel_id] = AppCommandChannel(state=state, data=channel_data, guild_id=guild_id) # type: ignore |
|||
|
|||
completed.update( |
|||
{ |
|||
attachment_id: Attachment(data=attachment_data, state=state) |
|||
for attachment_id, attachment_data in resolved.get('attachments', {}).items() |
|||
} |
|||
) |
|||
|
|||
# TODO: messages |
|||
|
|||
for option in options: |
|||
opt_type = option['type'] |
|||
name = option['name'] |
|||
if opt_type in (3, 4, 5): # string, integer, boolean |
|||
value = option['value'] # type: ignore -- Key is there |
|||
self.__dict__[name] = value |
|||
elif opt_type == 10: # number |
|||
value = option['value'] # type: ignore -- Key is there |
|||
if value is None: |
|||
self.__dict__[name] = float('nan') |
|||
else: |
|||
self.__dict__[name] = float(value) |
|||
elif opt_type in (6, 7, 8, 9, 11): |
|||
# Remaining ones should be snowflake based ones with resolved data |
|||
snowflake: str = option['value'] # type: ignore -- Key is there |
|||
value = completed.get(snowflake) |
|||
self.__dict__[name] = value |
|||
|
|||
def __repr__(self) -> str: |
|||
items = (f'{k}={v!r}' for k, v in self.__dict__.items()) |
|||
return '<{} {}>'.format(self.__class__.__name__, ' '.join(items)) |
|||
|
|||
def __eq__(self, other: object) -> bool: |
|||
if isinstance(self, Namespace) and isinstance(other, Namespace): |
|||
return self.__dict__ == other.__dict__ |
|||
return NotImplemented |
|||
|
|||
def _update_with_defaults(self, defaults: Iterable[Tuple[str, Any]]) -> None: |
|||
for key, value in defaults: |
|||
self.__dict__.setdefault(key, value) |
@ -0,0 +1,416 @@ |
|||
""" |
|||
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 Callable, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, Union |
|||
|
|||
|
|||
from .namespace import Namespace |
|||
from .models import AppCommand |
|||
from .commands import Command, Group, _shorten |
|||
from .enums import AppCommandType |
|||
from .errors import CommandAlreadyRegistered, CommandNotFound, CommandSignatureMismatch |
|||
from ..errors import ClientException |
|||
from ..utils import MISSING |
|||
|
|||
if TYPE_CHECKING: |
|||
from ..types.interactions import ApplicationCommandInteractionData, ApplicationCommandInteractionDataOption |
|||
from ..interactions import Interaction |
|||
from ..client import Client |
|||
from ..abc import Snowflake |
|||
from .commands import CommandCallback, P, T |
|||
|
|||
__all__ = ('CommandTree',) |
|||
|
|||
|
|||
class CommandTree: |
|||
"""Represents a container that holds application command information. |
|||
|
|||
Parameters |
|||
----------- |
|||
client: :class:`Client` |
|||
The client instance to get application command information from. |
|||
""" |
|||
|
|||
def __init__(self, client: Client): |
|||
self.client = client |
|||
self._http = client.http |
|||
self._state = client._connection |
|||
self._state._command_tree = self |
|||
self._guild_commands: Dict[int, Dict[str, Union[Command, Group]]] = {} |
|||
self._global_commands: Dict[str, Union[Command, Group]] = {} |
|||
# (name, guild_id, command_type): Command |
|||
# The above two mappings can use this structure too but we need fast retrieval |
|||
# by name and guild_id in the above case while here it isn't as important since |
|||
# it's uncommon and N=5 anyway. |
|||
self._context_menus: Dict[Tuple[str, Optional[int], int], Command] = {} |
|||
|
|||
async def fetch_commands(self, *, guild: Optional[Snowflake] = None) -> List[AppCommand]: |
|||
"""|coro| |
|||
|
|||
Fetches the application's current commands. |
|||
|
|||
If no guild is passed then global commands are fetched, otherwise |
|||
the guild's commands are fetched instead. |
|||
|
|||
Parameters |
|||
----------- |
|||
guild: Optional[:class:`abc.Snowflake`] |
|||
The guild to fetch the commands from. If not passed then global commands |
|||
are fetched instead. |
|||
|
|||
Raises |
|||
------- |
|||
HTTPException |
|||
Fetching the commands failed. |
|||
ClientException |
|||
The application ID could not be found. |
|||
|
|||
Returns |
|||
-------- |
|||
List[:class:`~discord.app_commands.AppCommand`] |
|||
The application's commands. |
|||
""" |
|||
if self.client.application_id is None: |
|||
raise ClientException('Client does not have an application ID set') |
|||
|
|||
if guild is None: |
|||
commands = await self._http.get_global_commands(self.client.application_id) |
|||
else: |
|||
commands = await self._http.get_guild_commands(self.client.application_id, guild.id) |
|||
|
|||
return [AppCommand(data=data, state=self._state) for data in commands] |
|||
|
|||
def add_command(self, command: Union[Command, Group], /, *, guild: Optional[Snowflake] = None, override: bool = False): |
|||
"""Adds an application command to the tree. |
|||
|
|||
This only adds the command locally -- in order to sync the commands |
|||
and enable them in the client, :meth:`sync` must be called. |
|||
|
|||
The root parent of the command is added regardless of the type passed. |
|||
|
|||
Parameters |
|||
----------- |
|||
command: Union[:class:`Command`, :class:`Group`] |
|||
The application command or group to add. |
|||
guild: Optional[:class:`abc.Snowflake`] |
|||
The guild to add the command to. If not given then it |
|||
becomes a global command instead. |
|||
override: :class:`bool` |
|||
Whether to override a command with the same name. If ``False`` |
|||
an exception is raised. Default is ``False``. |
|||
|
|||
Raises |
|||
-------- |
|||
~discord.CommandAlreadyRegistered |
|||
The command was already registered and no override was specified. |
|||
TypeError |
|||
The application command passed is not a valid application command. |
|||
ValueError |
|||
The maximum number of commands was reached globally or for that guild. |
|||
This is currently 100 for slash commands and 5 for context menu commands. |
|||
""" |
|||
|
|||
if not isinstance(command, (Command, Group)): |
|||
raise TypeError(f'Expected a application command, received {command.__class__!r} instead') |
|||
|
|||
# todo: validate application command groups having children (required) |
|||
|
|||
root = command.root_parent or command |
|||
name = root.name |
|||
if guild is not None: |
|||
commands = self._guild_commands.setdefault(guild.id, {}) |
|||
found = name in commands |
|||
if found and not override: |
|||
raise CommandAlreadyRegistered(name, guild.id) |
|||
if len(commands) + found > 100: |
|||
raise ValueError('maximum number of slash commands exceeded (100)') |
|||
commands[name] = root |
|||
else: |
|||
found = name in self._global_commands |
|||
if found and not override: |
|||
raise CommandAlreadyRegistered(name, None) |
|||
if len(self._global_commands) + found > 100: |
|||
raise ValueError('maximum number of slash commands exceeded (100)') |
|||
self._global_commands[name] = root |
|||
|
|||
def remove_command(self, command: str, /, *, guild: Optional[Snowflake] = None) -> Optional[Union[Command, Group]]: |
|||
"""Removes an application command from the tree. |
|||
|
|||
This only removes the command locally -- in order to sync the commands |
|||
and remove them in the client, :meth:`sync` must be called. |
|||
|
|||
Parameters |
|||
----------- |
|||
command: :class:`str` |
|||
The name of the root command to remove. |
|||
guild: Optional[:class:`abc.Snowflake`] |
|||
The guild to remove the command from. If not given then it |
|||
removes a global command instead. |
|||
|
|||
Returns |
|||
--------- |
|||
Optional[Union[:class:`Command`, :class:`Group`]] |
|||
The application command that got removed. |
|||
If nothing was removed then ``None`` is returned instead. |
|||
""" |
|||
|
|||
if guild is None: |
|||
return self._global_commands.pop(command, None) |
|||
else: |
|||
try: |
|||
commands = self._guild_commands[guild.id] |
|||
except KeyError: |
|||
return None |
|||
else: |
|||
return commands.pop(command, None) |
|||
|
|||
def get_command(self, command: str, /, *, guild: Optional[Snowflake] = None) -> Optional[Union[Command, Group]]: |
|||
"""Gets a application command from the tree. |
|||
|
|||
.. note:: |
|||
|
|||
This does *not* include context menu commands. |
|||
|
|||
Parameters |
|||
----------- |
|||
command: :class:`str` |
|||
The name of the root command to get. |
|||
guild: Optional[:class:`abc.Snowflake`] |
|||
The guild to get the command from. If not given then it |
|||
gets a global command instead. |
|||
|
|||
Returns |
|||
--------- |
|||
Optional[Union[:class:`Command`, :class:`Group`]] |
|||
The application command that was found. |
|||
If nothing was found then ``None`` is returned instead. |
|||
""" |
|||
|
|||
if guild is None: |
|||
return self._global_commands.get(command) |
|||
else: |
|||
try: |
|||
commands = self._guild_commands[guild.id] |
|||
except KeyError: |
|||
return None |
|||
else: |
|||
return commands.get(command) |
|||
|
|||
def get_commands(self, *, guild: Optional[Snowflake] = None) -> List[Union[Command, Group]]: |
|||
"""Gets all application commands from the tree. |
|||
|
|||
.. note:: |
|||
|
|||
This does *not* retrieve context menu commands. |
|||
|
|||
Parameters |
|||
----------- |
|||
guild: Optional[:class:`~discord.abc.Snowflake`] |
|||
The guild to get the commands from. If not given then it |
|||
gets all global commands instead. |
|||
|
|||
Returns |
|||
--------- |
|||
List[Union[:class:`Command`, :class:`Group`]] |
|||
The application commands from the tree. |
|||
""" |
|||
|
|||
if guild is None: |
|||
return list(self._global_commands.values()) |
|||
else: |
|||
try: |
|||
commands = self._guild_commands[guild.id] |
|||
except KeyError: |
|||
return [] |
|||
else: |
|||
return list(commands.values()) |
|||
|
|||
def command( |
|||
self, |
|||
*, |
|||
name: str = MISSING, |
|||
description: str = MISSING, |
|||
guild: Optional[Snowflake] = None, |
|||
) -> Callable[[CommandCallback[Group, P, T]], Command[Group, P, T]]: |
|||
"""Creates an application command directly under this tree. |
|||
|
|||
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. |
|||
guild: Optional[:class:`Snowflake`] |
|||
The guild to add the command to. If not given then it |
|||
becomes a global command instead. |
|||
""" |
|||
|
|||
def decorator(func: CommandCallback[Group, P, T]) -> Command[Group, 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, |
|||
type=AppCommandType.chat_input, |
|||
parent=None, |
|||
) |
|||
self.add_command(command, guild=guild) |
|||
return command |
|||
|
|||
return decorator |
|||
|
|||
async def sync(self, *, guild: Optional[Snowflake]) -> List[AppCommand]: |
|||
"""|coro| |
|||
|
|||
Syncs the application commands to Discord. |
|||
|
|||
This must be called for the application commands to show up. |
|||
|
|||
Global commands take up to 1-hour to propagate but guild |
|||
commands propagate instantly. |
|||
|
|||
Parameters |
|||
----------- |
|||
guild: Optional[:class:`~discord.abc.Snowflake`] |
|||
The guild to sync the commands to. If ``None`` then it |
|||
syncs all global commands instead. |
|||
|
|||
Raises |
|||
------- |
|||
HTTPException |
|||
Syncing the commands failed. |
|||
ClientException |
|||
The client does not have an application ID. |
|||
|
|||
Returns |
|||
-------- |
|||
List[:class:`~discord.AppCommand`] |
|||
The application's commands that got synced. |
|||
""" |
|||
|
|||
if self.client.application_id is None: |
|||
raise ClientException('Client does not have an application ID set') |
|||
|
|||
commands = self.get_commands(guild=guild) |
|||
payload = [command.to_dict() for command in commands] |
|||
if guild is None: |
|||
data = await self._http.bulk_upsert_global_commands(self.client.application_id, payload=payload) |
|||
else: |
|||
data = await self._http.bulk_upsert_guild_commands(self.client.application_id, guild.id, payload=payload) |
|||
|
|||
return [AppCommand(data=d, state=self._state) for d in data] |
|||
|
|||
def _from_interaction(self, interaction: Interaction): |
|||
async def wrapper(): |
|||
try: |
|||
await self.call(interaction) |
|||
except Exception as e: |
|||
print(f'Error:', e) |
|||
|
|||
self.client.loop.create_task(wrapper(), name='CommandTree-invoker') |
|||
|
|||
async def call(self, interaction: Interaction): |
|||
"""|coro| |
|||
|
|||
Given an :class:`~discord.Interaction`, calls the matching |
|||
application command that's being invoked. |
|||
|
|||
This is usually called automatically by the library. |
|||
|
|||
Parameters |
|||
----------- |
|||
interaction: :class:`~discord.Interaction` |
|||
The interaction to dispatch from. |
|||
|
|||
Raises |
|||
-------- |
|||
CommandNotFound |
|||
The application command referred to could not be found. |
|||
CommandSignatureMismatch |
|||
The interaction data referred to a parameter that was not found in the |
|||
application command definition. |
|||
""" |
|||
data: ApplicationCommandInteractionData = interaction.data # type: ignore |
|||
parents: List[str] = [] |
|||
name = data['name'] |
|||
command = self._global_commands.get(name) |
|||
if interaction.guild_id: |
|||
try: |
|||
guild_commands = self._guild_commands[interaction.guild_id] |
|||
except KeyError: |
|||
pass |
|||
else: |
|||
command = guild_commands.get(name) or command |
|||
|
|||
# If it's not found at this point then it's not gonna be found at any point |
|||
if command is None: |
|||
raise CommandNotFound(name, parents) |
|||
|
|||
# This could be done recursively but it'd be a bother due to the state needed |
|||
# to be tracked above like the parents, the actual command type, and the |
|||
# resulting options we care about |
|||
searching = True |
|||
options: List[ApplicationCommandInteractionDataOption] = data.get('options', []) |
|||
while searching: |
|||
for option in options: |
|||
# Find subcommands |
|||
if option.get('type', 0) in (1, 2): |
|||
parents.append(name) |
|||
name = option['name'] |
|||
command = command._get_internal_command(name) |
|||
if command is None: |
|||
raise CommandNotFound(name, parents) |
|||
options = option.get('options', []) |
|||
break |
|||
else: |
|||
searching = False |
|||
break |
|||
else: |
|||
break |
|||
|
|||
if isinstance(command, Group): |
|||
# Right now, groups can't be invoked. This is a Discord limitation in how they |
|||
# do slash commands. So if we're here and we have a Group rather than a Command instance |
|||
# then something in the code is out of date from the data that Discord has. |
|||
raise CommandSignatureMismatch(command) |
|||
|
|||
# At this point options refers to the arguments of the command |
|||
# and command refers to the class type we care about |
|||
namespace = Namespace(interaction, data.get('resolved', {}), options) |
|||
await command._invoke_with_namespace(interaction, namespace) |
Loading…
Reference in new issue