mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-06-08 04:38:42 +00:00
Implement slash commands
This commit is contained in:
parent
3d0c506124
commit
0d2db90028
@ -43,7 +43,7 @@ from .template import *
|
|||||||
from .widget import *
|
from .widget import *
|
||||||
from .object import *
|
from .object import *
|
||||||
from .reaction import *
|
from .reaction import *
|
||||||
from . import utils, opus, abc, ui
|
from . import utils, opus, abc, ui, app_commands
|
||||||
from .enums import *
|
from .enums import *
|
||||||
from .embeds import *
|
from .embeds import *
|
||||||
from .mentions import *
|
from .mentions import *
|
||||||
|
16
discord/app_commands/__init__.py
Normal file
16
discord/app_commands/__init__.py
Normal file
@ -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 *
|
743
discord/app_commands/commands.py
Normal file
743
discord/app_commands/commands.py
Normal file
@ -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
|
53
discord/app_commands/enums.py
Normal file
53
discord/app_commands/enums.py
Normal file
@ -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
|
96
discord/app_commands/errors.py
Normal file
96
discord/app_commands/errors.py
Normal file
@ -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)
|
592
discord/app_commands/models.py
Normal file
592
discord/app_commands/models.py
Normal file
@ -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)
|
160
discord/app_commands/namespace.py
Normal file
160
discord/app_commands/namespace.py
Normal file
@ -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)
|
416
discord/app_commands/tree.py
Normal file
416
discord/app_commands/tree.py
Normal file
@ -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)
|
@ -69,6 +69,7 @@ if TYPE_CHECKING:
|
|||||||
from .voice_client import VoiceProtocol
|
from .voice_client import VoiceProtocol
|
||||||
from .client import Client
|
from .client import Client
|
||||||
from .gateway import DiscordWebSocket
|
from .gateway import DiscordWebSocket
|
||||||
|
from .app_commands import CommandTree
|
||||||
|
|
||||||
from .types.snowflake import Snowflake
|
from .types.snowflake import Snowflake
|
||||||
from .types.activity import Activity as ActivityPayload
|
from .types.activity import Activity as ActivityPayload
|
||||||
@ -227,6 +228,7 @@ class ConnectionState:
|
|||||||
self._activity: Optional[ActivityPayload] = activity
|
self._activity: Optional[ActivityPayload] = activity
|
||||||
self._status: Optional[str] = status
|
self._status: Optional[str] = status
|
||||||
self._intents: Intents = intents
|
self._intents: Intents = intents
|
||||||
|
self._command_tree: Optional[CommandTree] = None
|
||||||
|
|
||||||
if not intents.members or cache_flags._empty:
|
if not intents.members or cache_flags._empty:
|
||||||
self.store_user = self.store_user_no_intents # type: ignore - This reassignment is on purpose
|
self.store_user = self.store_user_no_intents # type: ignore - This reassignment is on purpose
|
||||||
@ -690,7 +692,9 @@ class ConnectionState:
|
|||||||
|
|
||||||
def parse_interaction_create(self, data: gw.InteractionCreateEvent) -> None:
|
def parse_interaction_create(self, data: gw.InteractionCreateEvent) -> None:
|
||||||
interaction = Interaction(data=data, state=self)
|
interaction = Interaction(data=data, state=self)
|
||||||
if data['type'] == 3: # interaction component
|
if data['type'] == 2 and self._command_tree: # application command
|
||||||
|
self._command_tree._from_interaction(interaction)
|
||||||
|
elif data['type'] == 3: # interaction component
|
||||||
# These keys are always there for this interaction type
|
# These keys are always there for this interaction type
|
||||||
custom_id = interaction.data['custom_id'] # type: ignore
|
custom_id = interaction.data['custom_id'] # type: ignore
|
||||||
component_type = interaction.data['component_type'] # type: ignore
|
component_type = interaction.data['component_type'] # type: ignore
|
||||||
|
Loading…
x
Reference in New Issue
Block a user