mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-09-05 09:26:10 +00:00
Implement slash commands
This commit is contained in:
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)
|
Reference in New Issue
Block a user