Browse Source

Fix various app command bugs, improve documentation, implement missing fields

pull/10109/head
dolfies 3 years ago
parent
commit
68d1b40239
  1. 297
      discord/abc.py
  2. 282
      discord/commands.py
  3. 4
      discord/components.py
  4. 8
      discord/ext/commands/context.py
  5. 2
      discord/http.py
  6. 90
      discord/message.py
  7. 2
      discord/modal.py
  8. 75
      docs/api.rst
  9. 13
      docs/ext/commands/api.rst

297
discord/abc.py

@ -33,6 +33,7 @@ from typing import (
Callable, Callable,
Dict, Dict,
List, List,
Literal,
Optional, Optional,
TYPE_CHECKING, TYPE_CHECKING,
Protocol, Protocol,
@ -57,7 +58,7 @@ from .http import handle_message_parameters
from .voice_client import VoiceClient, VoiceProtocol from .voice_client import VoiceClient, VoiceProtocol
from .sticker import GuildSticker, StickerItem from .sticker import GuildSticker, StickerItem
from .settings import ChannelSettings from .settings import ChannelSettings
from .commands import ApplicationCommand, SlashCommand, UserCommand from .commands import ApplicationCommand, BaseCommand, SlashCommand, UserCommand, MessageCommand, _command_factory
from . import utils from . import utils
__all__ = ( __all__ = (
@ -145,6 +146,127 @@ async def _purge_helper(
return ret return ret
@overload
def _handle_commands(
messageable: Messageable,
type: Literal[AppCommandType.chat_input],
*,
query: Optional[str] = ...,
limit: Optional[int] = ...,
command_ids: Optional[List[int]] = ...,
application: Optional[Snowflake] = ...,
include_applications: bool = ...,
target: Optional[Snowflake] = ...,
) -> AsyncIterator[SlashCommand]:
...
@overload
def _handle_commands(
messageable: Messageable,
type: Literal[AppCommandType.user],
*,
query: Optional[str] = ...,
limit: Optional[int] = ...,
command_ids: Optional[List[int]] = ...,
application: Optional[Snowflake] = ...,
include_applications: bool = ...,
target: Optional[Snowflake] = ...,
) -> AsyncIterator[UserCommand]:
...
@overload
def _handle_commands(
messageable: Message,
type: Literal[AppCommandType.message],
*,
query: Optional[str] = ...,
limit: Optional[int] = ...,
command_ids: Optional[List[int]] = ...,
application: Optional[Snowflake] = ...,
include_applications: bool = ...,
target: Optional[Snowflake] = ...,
) -> AsyncIterator[MessageCommand]:
...
async def _handle_commands(
messageable: Union[Messageable, Message],
type: AppCommandType,
*,
query: Optional[str] = None,
limit: Optional[int] = None,
command_ids: Optional[List[int]] = None,
application: Optional[Snowflake] = None,
include_applications: bool = True,
target: Optional[Snowflake] = None,
) -> AsyncIterator[BaseCommand]:
if limit is not None and limit < 0:
raise ValueError('limit must be greater than or equal to 0')
if query and command_ids:
raise TypeError('Cannot specify both query and command_ids')
state = messageable._state
endpoint = state.http.search_application_commands
channel = await messageable._get_channel()
application_id = application.id if application else None
if channel.type == ChannelType.private:
recipient: User = channel.recipient # type: ignore
if not recipient.bot:
raise TypeError('Cannot fetch commands in a DM with a non-bot user')
application_id = recipient.id
target = recipient
elif channel.type == ChannelType.group:
return
while True:
# We keep two cursors because Discord just sends us an infinite loop sometimes
prev_cursor = MISSING
cursor = MISSING
retrieve = min((25 if not command_ids else 0) if limit is None else limit, 25)
if limit is not None:
limit -= retrieve
if (not command_ids and retrieve < 1) or cursor is None or (prev_cursor is not MISSING and prev_cursor == cursor):
return
data = await endpoint(
channel.id,
type.value,
limit=retrieve if not application_id else None,
query=query if not command_ids and not application_id else None,
command_ids=command_ids if not application_id else None, # type: ignore
application_id=application_id,
include_applications=include_applications,
cursor=cursor,
)
prev_cursor = cursor
cursor = data['cursor'].get('next')
cmds = data['application_commands']
apps: Dict[int, dict] = {int(app['id']): app for app in data.get('applications') or []}
if len(cmds) <= min(limit if limit else 25, 25) or application_id:
limit = 0
for cmd in cmds:
# Handle faked parameters
if application_id and command_ids and int(cmd['id']) not in command_ids:
continue
elif application_id and query and query.lower() not in cmd['name']:
continue
elif application_id and limit == 0:
return
# We follow Discord behavior
if limit is not None and (not command_ids or int(cmd['id']) not in command_ids):
limit -= 1
cmd['application'] = apps.get(int(cmd['application_id']))
_, cls = _command_factory(type.value)
yield cls(state=state, data=cmd, channel=channel, target=target)
@runtime_checkable @runtime_checkable
class Snowflake(Protocol): class Snowflake(Protocol):
"""An ABC that details the common operations on a Discord model. """An ABC that details the common operations on a Discord model.
@ -194,6 +316,7 @@ class User(Snowflake, Protocol):
name: str name: str
discriminator: str discriminator: str
bot: bool bot: bool
system: bool
@property @property
def display_name(self) -> str: def display_name(self) -> str:
@ -1529,7 +1652,7 @@ class Messageable:
async def ack_pins(self) -> None: async def ack_pins(self) -> None:
"""|coro| """|coro|
Acks the channel's pins. Marks a channel's pins as viewed.
Raises Raises
------- -------
@ -1727,14 +1850,14 @@ class Messageable:
for raw_message in data: for raw_message in data:
yield self._state.create_message(channel=channel, data=raw_message) yield self._state.create_message(channel=channel, data=raw_message)
async def slash_commands( def slash_commands(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
*, *,
limit: Optional[int] = None, limit: Optional[int] = None,
command_ids: Optional[List[int]] = None, command_ids: Optional[List[int]] = None,
application: Optional[Snowflake] = None, application: Optional[Snowflake] = None,
include_applications: bool = MISSING, include_applications: bool = True,
) -> AsyncIterator[SlashCommand]: ) -> AsyncIterator[SlashCommand]:
"""Returns a :term:`asynchronous iterator` of the slash commands available in the channel. """Returns a :term:`asynchronous iterator` of the slash commands available in the channel.
@ -1760,23 +1883,24 @@ class Messageable:
This parameter is faked if ``application`` is specified. This parameter is faked if ``application`` is specified.
limit: Optional[:class:`int`] limit: Optional[:class:`int`]
The maximum number of commands to send back. Defaults to 25. If ``None``, returns all commands. The maximum number of commands to send back. Defaults to 0 if ``command_ids`` is passed, else 25.
If ``None``, returns all commands.
This parameter is faked if ``application`` is specified. This parameter is faked if ``application`` is specified.
command_ids: Optional[List[:class:`int`]] command_ids: Optional[List[:class:`int`]]
List of up to 100 command IDs to search for. If the command doesn't exist, it won't be returned. List of up to 100 command IDs to search for. If the command doesn't exist, it won't be returned.
If ``limit`` is passed alongside this parameter, this parameter will serve as a "preferred commands" list.
This means that the endpoint will return the found commands + ``limit`` more, if available.
application: Optional[:class:`~discord.abc.Snowflake`] application: Optional[:class:`~discord.abc.Snowflake`]
Return this application's commands. Always set to DM recipient in a private channel context. Whether to return this application's commands. Always set to DM recipient in a private channel context.
include_applications: :class:`bool` include_applications: :class:`bool`
Whether to include applications in the response. This defaults to ``True`` if possible. Whether to include applications in the response. Defaults to ``True``.
Cannot be set to ``True`` if ``application`` is specified.
Raises Raises
------ ------
TypeError TypeError
Both query and command_ids are passed. Both query and command_ids are passed.
Both application and include_applications are passed.
Attempted to fetch commands in a DM with a non-bot user. Attempted to fetch commands in a DM with a non-bot user.
ValueError ValueError
The limit was not greater than or equal to 0. The limit was not greater than or equal to 0.
@ -1792,76 +1916,24 @@ class Messageable:
:class:`.SlashCommand` :class:`.SlashCommand`
A slash command. A slash command.
""" """
if limit is not None and limit < 0: return _handle_commands(
raise ValueError('limit must be greater than or equal to 0') self,
if query and command_ids: AppCommandType.chat_input,
raise TypeError('Cannot specify both query and command_ids') query=query,
limit=limit,
state = self._state command_ids=command_ids,
endpoint = state.http.search_application_commands application=application,
channel = await self._get_channel() include_applications=include_applications,
application_id = application.id if application else None
if channel.type == ChannelType.private:
recipient: User = channel.recipient # type: ignore
if not recipient.bot:
raise TypeError('Cannot fetch commands in a DM with a non-bot user')
application_id = recipient.id
elif channel.type == ChannelType.group:
return
if application_id and include_applications:
raise TypeError('Cannot specify both application and include_applications')
include_applications = (
(False if application_id else True) if include_applications is MISSING else include_applications
)
while True:
cursor = MISSING
retrieve = min(25 if limit is None else limit, 25)
if retrieve < 1 or cursor is None:
return
data = await endpoint(
channel.id,
AppCommandType.chat_input.value,
limit=retrieve if not application_id else None,
query=query if not command_ids and not application_id else None,
command_ids=command_ids if not application_id else None, # type: ignore
application_id=application_id,
include_applications=include_applications if not application_id else None,
cursor=cursor if cursor is not MISSING else None,
) )
cursor = data['cursor'].get('next')
cmds = data['application_commands']
apps: Dict[int, dict] = {int(app['id']): app for app in data.get('applications') or []}
if len(cmds) < 25:
limit = 0
for cmd in cmds: def user_commands(
# Handle faked parameters
if command_ids and int(cmd['id']) not in command_ids:
continue
elif application_id and query and query.lower() not in cmd['name']:
continue
elif application_id and limit == 0:
return
if limit is not None:
limit -= 1
cmd['application'] = apps.get(int(cmd['application_id']))
yield SlashCommand(state=state, data=cmd, channel=channel)
async def user_commands(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
*, *,
limit: Optional[int] = None, limit: Optional[int] = None,
command_ids: Optional[List[int]] = None, command_ids: Optional[List[int]] = None,
application: Optional[Snowflake] = None, application: Optional[Snowflake] = None,
include_applications: bool = MISSING, include_applications: bool = True,
) -> AsyncIterator[UserCommand]: ) -> AsyncIterator[UserCommand]:
"""Returns a :term:`asynchronous iterator` of the user commands available to use on the user. """Returns a :term:`asynchronous iterator` of the user commands available to use on the user.
@ -1887,23 +1959,24 @@ class Messageable:
This parameter is faked if ``application`` is specified. This parameter is faked if ``application`` is specified.
limit: Optional[:class:`int`] limit: Optional[:class:`int`]
The maximum number of commands to send back. Defaults to 25. If ``None``, returns all commands. The maximum number of commands to send back. Defaults to 0 if ``command_ids`` is passed, else 25.
If ``None``, returns all commands.
This parameter is faked if ``application`` is specified. This parameter is faked if ``application`` is specified.
command_ids: Optional[List[:class:`int`]] command_ids: Optional[List[:class:`int`]]
List of up to 100 command IDs to search for. If the command doesn't exist, it won't be returned. List of up to 100 command IDs to search for. If the command doesn't exist, it won't be returned.
If ``limit`` is passed alongside this parameter, this parameter will serve as a "preferred commands" list.
This means that the endpoint will return the found commands + ``limit`` more, if available.
application: Optional[:class:`~discord.abc.Snowflake`] application: Optional[:class:`~discord.abc.Snowflake`]
Return this application's commands. Always set to DM recipient in a private channel context. Whether to return this application's commands. Always set to DM recipient in a private channel context.
include_applications: :class:`bool` include_applications: :class:`bool`
Whether to include applications in the response. This defaults to ``True`` if possible. Whether to include applications in the response. Defaults to ``True``.
Cannot be set to ``True`` if ``application`` is specified.
Raises Raises
------ ------
TypeError TypeError
Both query and command_ids are passed. Both query and command_ids are passed.
Both application and include_applications are passed.
Attempted to fetch commands in a DM with a non-bot user. Attempted to fetch commands in a DM with a non-bot user.
ValueError ValueError
The limit was not greater than or equal to 0. The limit was not greater than or equal to 0.
@ -1919,67 +1992,15 @@ class Messageable:
:class:`.UserCommand` :class:`.UserCommand`
A user command. A user command.
""" """
if limit is not None and limit < 0: return _handle_commands(
raise ValueError('limit must be greater than or equal to 0') self,
if query and command_ids: AppCommandType.user,
raise TypeError('Cannot specify both query and command_ids') query=query,
limit=limit,
state = self._state command_ids=command_ids,
endpoint = state.http.search_application_commands application=application,
channel = await self._get_channel() include_applications=include_applications,
application_id = application.id if application else None
if channel.type == ChannelType.private:
recipient: User = channel.recipient # type: ignore
if not recipient.bot:
raise TypeError('Cannot fetch commands in a DM with a non-bot user')
application_id = recipient.id
elif channel.type == ChannelType.group:
return
if application_id and include_applications:
raise TypeError('Cannot specify both application and include_applications')
include_applications = (
(False if application_id else True) if include_applications is MISSING else include_applications
)
while True:
cursor = MISSING
retrieve = min(25 if limit is None else limit, 25)
if retrieve < 1 or cursor is None:
return
data = await endpoint(
channel.id,
AppCommandType.user.value,
limit=retrieve if not application_id else None,
query=query if not command_ids and not application_id else None,
command_ids=command_ids if not application_id else None, # type: ignore
application_id=application_id,
include_applications=include_applications if not application_id else None,
cursor=cursor if cursor is not MISSING else None,
) )
cursor = data['cursor'].get('next')
cmds = data['application_commands']
apps: Dict[int, dict] = {int(app['id']): app for app in data.get('applications') or []}
if len(cmds) < 25:
limit = 0
for cmd in cmds:
# Handle faked parameters
if command_ids and int(cmd['id']) not in command_ids:
continue
elif application_id and query and query.lower() not in cmd['name'].lower():
continue
elif application_id and limit == 0:
return
if limit is not None:
limit -= 1
cmd['application'] = apps.get(int(cmd['application_id']))
yield UserCommand(state=state, data=cmd, channel=channel, user=getattr(channel, 'recipient', None))
class Connectable(Protocol): class Connectable(Protocol):

282
discord/commands.py

@ -36,10 +36,12 @@ if TYPE_CHECKING:
from .abc import Messageable, Snowflake from .abc import Messageable, Snowflake
from .appinfo import InteractionApplication from .appinfo import InteractionApplication
from .file import File from .file import File
from .guild import Guild
from .interactions import Interaction from .interactions import Interaction
from .message import Attachment, Message from .message import Attachment, Message
from .state import ConnectionState from .state import ConnectionState
__all__ = ( __all__ = (
'BaseCommand', 'BaseCommand',
'UserCommand', 'UserCommand',
@ -57,12 +59,13 @@ class ApplicationCommand(Protocol):
The following implement this ABC: The following implement this ABC:
- :class:`~discord.BaseCommand`
- :class:`~discord.UserCommand` - :class:`~discord.UserCommand`
- :class:`~discord.MessageCommand` - :class:`~discord.MessageCommand`
- :class:`~discord.SlashCommand` - :class:`~discord.SlashCommand`
- :class:`~discord.SubCommand` - :class:`~discord.SubCommand`
.. versionadded:: 2.0
Attributes Attributes
----------- -----------
name: :class:`str` name: :class:`str`
@ -75,11 +78,16 @@ class ApplicationCommand(Protocol):
Whether the command is enabled in guilds by default. Whether the command is enabled in guilds by default.
dm_permission: :class:`bool` dm_permission: :class:`bool`
Whether the command is enabled in DMs. Whether the command is enabled in DMs.
nsfw: :class:`bool`
Whether the command is marked NSFW and only available in NSFW channels.
application: Optional[:class:`~discord.InteractionApplication`] application: Optional[:class:`~discord.InteractionApplication`]
The application this command belongs to. The application this command belongs to.
Only available if requested. Only available if requested.
application_id: :class:`int` application_id: :class:`int`
The ID of the application this command belongs to. The ID of the application this command belongs to.
guild_id: Optional[:class:`int`]
The ID of the guild this command is registered in. A value of ``None``
denotes that it is a global command.
""" """
__slots__ = () __slots__ = ()
@ -94,8 +102,11 @@ class ApplicationCommand(Protocol):
type: AppCommandType type: AppCommandType
default_permission: bool default_permission: bool
dm_permission: bool dm_permission: bool
nsfw: bool
application_id: int application_id: int
application: Optional[InteractionApplication] application: Optional[InteractionApplication]
mention: str
guild_id: Optional[int]
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@ -117,7 +128,7 @@ class ApplicationCommand(Protocol):
i = await state.client.wait_for( i = await state.client.wait_for(
'interaction_finish', 'interaction_finish',
check=lambda d: d.nonce == nonce, check=lambda d: d.nonce == nonce,
timeout=7, timeout=6,
) )
except TimeoutError as exc: except TimeoutError as exc:
raise InvalidData('Did not receive a response from Discord') from exc raise InvalidData('Did not receive a response from Discord') from exc
@ -125,6 +136,13 @@ class ApplicationCommand(Protocol):
state._interaction_cache.pop(nonce, None) state._interaction_cache.pop(nonce, None)
return i return i
@property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`~discord.Guild`]: Returns the guild this command is registered to
if it exists.
"""
return self._state._get_guild(self.guild_id)
def is_group(self) -> bool: def is_group(self) -> bool:
"""Query whether this command is a group. """Query whether this command is a group.
@ -163,49 +181,6 @@ class ApplicationCommand(Protocol):
class BaseCommand(ApplicationCommand, Hashable): class BaseCommand(ApplicationCommand, Hashable):
"""Represents a base command.
.. container:: operations
.. describe:: x == y
Checks if two commands are equal.
.. describe:: x != y
Checks if two commands are not equal.
.. describe:: hash(x)
Return the command's hash.
.. describe:: str(x)
Returns the command's name.
Attributes
----------
id: :class:`int`
The command's ID.
version: :class:`int`
The command's version.
name: :class:`str`
The command's name.
description: :class:`str`
The command's description, if any.
type: :class:`AppCommandType`
The type of application command.
default_permission: :class:`bool`
Whether the command is enabled in guilds by default.
dm_permission: :class:`bool`
Whether the command is enabled in DMs.
application: Optional[:class:`InteractionApplication`]
The application this command belongs to.
Only available if requested.
application_id: :class:`int`
The ID of the application this command belongs to.
"""
__slots__ = ( __slots__ = (
'name', 'name',
'description', 'description',
@ -216,13 +191,15 @@ class BaseCommand(ApplicationCommand, Hashable):
'application', 'application',
'application_id', 'application_id',
'dm_permission', 'dm_permission',
'nsfw',
'guild_id',
'_data', '_data',
'_state', '_state',
'_channel', '_channel',
'_default_member_permissions', '_default_member_permissions',
) )
def __init__(self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None) -> None: def __init__(self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None, **kwargs) -> None:
self._state = state self._state = state
self._data = data self._data = data
self.name = data['name'] self.name = data['name']
@ -240,10 +217,17 @@ class BaseCommand(ApplicationCommand, Hashable):
self.default_permission: bool = data.get('default_permission', True) self.default_permission: bool = data.get('default_permission', True)
dm_permission = data.get('dm_permission') # Null means true? dm_permission = data.get('dm_permission') # Null means true?
self.dm_permission = dm_permission if dm_permission is not None else True self.dm_permission = dm_permission if dm_permission is not None else True
self.nsfw: bool = data.get('nsfw', False)
self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id')
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<{self.__class__.__name__} id={self.id} name={self.name!r}>' return f'<{self.__class__.__name__} id={self.id} name={self.name!r}>'
@property
def mention(self) -> str:
""":class:`str`: Returns a string that allows you to mention the command."""
return f'</{self.name}:{self.id}>'
class SlashMixin(ApplicationCommand, Protocol): class SlashMixin(ApplicationCommand, Protocol):
if TYPE_CHECKING: if TYPE_CHECKING:
@ -270,6 +254,8 @@ class SlashMixin(ApplicationCommand, Protocol):
'type': obj.type.value, 'type': obj.type.value,
'version': str(obj.version), 'version': str(obj.version),
} }
if self.guild_id:
data['guild_id'] = str(self.guild_id)
return await super().__call__(data, files, channel) return await super().__call__(data, files, channel)
def _parse_kwargs(self, kwargs: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], List[File], List[Attachment]]: def _parse_kwargs(self, kwargs: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], List[File], List[Attachment]]:
@ -330,13 +316,63 @@ class SlashMixin(ApplicationCommand, Protocol):
class UserCommand(BaseCommand): class UserCommand(BaseCommand):
"""Represents a user command.""" """Represents a user command.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two commands are equal.
.. describe:: x != y
Checks if two commands are not equal.
.. describe:: hash(x)
Return the command's hash.
.. describe:: str(x)
Returns the command's name.
Attributes
----------
id: :class:`int`
The command's ID.
version: :class:`int`
The command's version.
name: :class:`str`
The command's name.
description: :class:`str`
The command's description, if any.
type: :class:`AppCommandType`
The type of application command. This will always be :attr:`AppCommandType.user`.
default_permission: :class:`bool`
Whether the command is enabled in guilds by default.
dm_permission: :class:`bool`
Whether the command is enabled in DMs.
nsfw: :class:`bool`
Whether the command is marked NSFW and only available in NSFW channels.
application: Optional[:class:`InteractionApplication`]
The application this command belongs to.
Only available if requested.
application_id: :class:`int`
The ID of the application this command belongs to.
guild_id: Optional[:class:`int`]
The ID of the guild this command is registered in. A value of ``None``
denotes that it is a global command.
.. automethod:: __call__
"""
__slots__ = ('_user',) __slots__ = ('_user',)
def __init__(self, *, user: Optional[Snowflake] = None, **kwargs): def __init__(self, *, target: Optional[Snowflake] = None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._user = user self._user = target
async def __call__(self, user: Optional[Snowflake] = None, *, channel: Optional[Messageable] = None): async def __call__(self, user: Optional[Snowflake] = None, *, channel: Optional[Messageable] = None):
"""Use the user command. """Use the user command.
@ -385,13 +421,63 @@ class UserCommand(BaseCommand):
class MessageCommand(BaseCommand): class MessageCommand(BaseCommand):
"""Represents a message command.""" """Represents a message command.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two commands are equal.
.. describe:: x != y
Checks if two commands are not equal.
.. describe:: hash(x)
Return the command's hash.
.. describe:: str(x)
Returns the command's name.
Attributes
----------
id: :class:`int`
The command's ID.
version: :class:`int`
The command's version.
name: :class:`str`
The command's name.
description: :class:`str`
The command's description, if any.
type: :class:`AppCommandType`
The type of application command. This will always be :attr:`AppCommandType.message`.
default_permission: :class:`bool`
Whether the command is enabled in guilds by default.
dm_permission: :class:`bool`
Whether the command is enabled in DMs.
nsfw: :class:`bool`
Whether the command is marked NSFW and only available in NSFW channels.
application: Optional[:class:`InteractionApplication`]
The application this command belongs to.
Only available if requested.
application_id: :class:`int`
The ID of the application this command belongs to.
guild_id: Optional[:class:`int`]
The ID of the guild this command is registered in. A value of ``None``
denotes that it is a global command.
.. automethod:: __call__
"""
__slots__ = ('_message',) __slots__ = ('_message',)
def __init__(self, *, message: Optional[Message] = None, **kwargs): def __init__(self, *, target: Optional[Message] = None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._message = message self._message = target
async def __call__(self, message: Optional[Message] = None, *, channel: Optional[Messageable] = None): async def __call__(self, message: Optional[Message] = None, *, channel: Optional[Messageable] = None):
"""Use the message command. """Use the message command.
@ -443,13 +529,58 @@ class MessageCommand(BaseCommand):
class SlashCommand(BaseCommand, SlashMixin): class SlashCommand(BaseCommand, SlashMixin):
"""Represents a slash command. """Represents a slash command.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two commands are equal.
.. describe:: x != y
Checks if two commands are not equal.
.. describe:: hash(x)
Return the command's hash.
.. describe:: str(x)
Returns the command's name.
Attributes Attributes
---------- ----------
id: :class:`int`
The command's ID.
version: :class:`int`
The command's version.
name: :class:`str`
The command's name.
description: :class:`str`
The command's description, if any.
type: :class:`AppCommandType`
The type of application command.
default_permission: :class:`bool`
Whether the command is enabled in guilds by default.
dm_permission: :class:`bool`
Whether the command is enabled in DMs.
nsfw: :class:`bool`
Whether the command is marked NSFW and only available in NSFW channels.
application: Optional[:class:`InteractionApplication`]
The application this command belongs to.
Only available if requested.
application_id: :class:`int`
The ID of the application this command belongs to.
guild_id: Optional[:class:`int`]
The ID of the guild this command is registered in. A value of ``None``
denotes that it is a global command.
options: List[:class:`Option`] options: List[:class:`Option`]
The command's options. The command's options.
children: List[:class:`SubCommand`] children: List[:class:`SubCommand`]
The command's subcommands. If a command has subcommands, it is a group and cannot be used. The command's subcommands. If a command has subcommands, it is a group and cannot be used.
You can access (and use) subcommands directly as attributes of the class.
.. automethod:: __call__
""" """
__slots__ = ('_parent', 'options', 'children') __slots__ = ('_parent', 'options', 'children')
@ -503,6 +634,8 @@ class SlashCommand(BaseCommand, SlashMixin):
class SubCommand(SlashMixin): class SubCommand(SlashMixin):
"""Represents a slash command child. """Represents a slash command child.
.. versionadded:: 2.0
This could be a subcommand, or a subgroup. This could be a subcommand, or a subgroup.
.. container:: operations .. container:: operations
@ -519,13 +652,14 @@ class SubCommand(SlashMixin):
The subcommand's description, if any. The subcommand's description, if any.
type: :class:`AppCommandType` type: :class:`AppCommandType`
The type of application command. Always :attr:`AppCommandType.chat_input`. The type of application command. Always :attr:`AppCommandType.chat_input`.
parent: :class:`SlashCommand` parent: Union[:class:`SlashCommand`, :class:`SubCommand`]
The parent command. The parent command.
options: List[:class:`Option`] options: List[:class:`Option`]
The subcommand's options. The subcommand's options.
children: List[:class:`SubCommand`] children: List[:class:`SubCommand`]
The subcommand's subcommands. If a subcommand has subcommands, it is a group and cannot be used. The subcommand's subcommands. If a subcommand has subcommands, it is a group and cannot be used.
You can access (and use) subcommands directly as attributes of the class.
.. automethod:: __call__
""" """
__slots__ = ( __slots__ = (
@ -608,6 +742,22 @@ class SubCommand(SlashMixin):
BASE += f' children={len(self.children)}' BASE += f' children={len(self.children)}'
return BASE + '>' return BASE + '>'
@property
def qualified_name(self) -> str:
""":class:`str`: Returns the fully qualified command name.
The qualified name includes the parent name as well. For example,
in a command like ``/foo bar`` the qualified name is ``foo bar``.
"""
names = [self.name, self.parent.name]
if isinstance(self.parent, SubCommand):
names.append(self._parent.name)
return ' '.join(reversed(names))
@property
def mention(self) -> str:
""":class:`str`: Returns a string that allows you to mention the subcommand."""
return f'</{self.qualified_name}:{self._parent.id}>'
@property @property
def _default_member_permissions(self) -> Optional[int]: def _default_member_permissions(self) -> Optional[int]:
return self._parent._default_member_permissions return self._parent._default_member_permissions
@ -632,6 +782,24 @@ class SubCommand(SlashMixin):
""":class:`bool`: Whether the command is enabled in DMs.""" """:class:`bool`: Whether the command is enabled in DMs."""
return self._parent.dm_permission return self._parent.dm_permission
@property
def nsfw(self) -> bool:
""":class:`bool`: Whether the command is marked NSFW and only available in NSFW channels."""
return self._parent.nsfw
@property
def guild_id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of the guild this command is registered in. A value of ``None``
denotes that it is a global command."""
return self._parent.guild_id
@property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`~discord.Guild`]: Returns the guild this command is registered to
if it exists.
"""
return self._parent.guild
def is_group(self) -> bool: def is_group(self) -> bool:
"""Query whether this command is a group. """Query whether this command is a group.
@ -665,6 +833,8 @@ class SubCommand(SlashMixin):
class Option: class Option:
"""Represents a command option. """Represents a command option.
.. versionadded:: 2.0
.. container:: operations .. container:: operations
.. describe:: str(x) .. describe:: str(x)
@ -739,6 +909,8 @@ class Option:
class OptionChoice: class OptionChoice:
"""Represents a choice for an option. """Represents a choice for an option.
.. versionadded:: 2.0
.. container:: operations .. container:: operations
.. describe:: str(x) .. describe:: str(x)

4
discord/components.py

@ -228,7 +228,7 @@ class Button(Component):
i = await state.client.wait_for( i = await state.client.wait_for(
'interaction_finish', 'interaction_finish',
check=lambda d: d.nonce == nonce, check=lambda d: d.nonce == nonce,
timeout=7, timeout=6,
) )
except TimeoutError as exc: except TimeoutError as exc:
raise InvalidData('Did not receive a response from Discord') from exc raise InvalidData('Did not receive a response from Discord') from exc
@ -324,7 +324,7 @@ class SelectMenu(Component):
i = await state.client.wait_for( i = await state.client.wait_for(
'interaction_finish', 'interaction_finish',
check=lambda d: d.nonce == nonce, check=lambda d: d.nonce == nonce,
timeout=7, timeout=6,
) )
except TimeoutError as exc: except TimeoutError as exc:
raise InvalidData('Did not receive a response from Discord') from exc raise InvalidData('Did not receive a response from Discord') from exc

8
discord/ext/commands/context.py

@ -411,7 +411,8 @@ class Context(discord.abc.Messageable, Generic[BotT]):
async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message:
return await self.message.reply(content, **kwargs) return await self.message.reply(content, **kwargs)
async def message_commands( @discord.utils.copy_doc(Message.message_commands)
def message_commands(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
*, *,
@ -420,7 +421,6 @@ class Context(discord.abc.Messageable, Generic[BotT]):
application: Optional[discord.abc.Snowflake] = None, application: Optional[discord.abc.Snowflake] = None,
include_applications: bool = True, include_applications: bool = True,
) -> AsyncIterator[MessageCommand]: ) -> AsyncIterator[MessageCommand]:
async for command in self.message.message_commands( return self.message.message_commands(
query, limit=limit, command_ids=command_ids, include_applications=include_applications, application=application query, limit=limit, command_ids=command_ids, include_applications=include_applications, application=application
): )
yield command

2
discord/http.py

@ -2389,7 +2389,7 @@ class HTTPClient:
if cursor: if cursor:
params['cursor'] = cursor params['cursor'] = cursor
if command_ids: if command_ids:
params['command_ids'] = command_ids params['command_ids'] = ','.join(map(str, command_ids))
if application_id: if application_id:
params['application_id'] = application_id params['application_id'] = application_id

90
discord/message.py

@ -66,6 +66,7 @@ from .threads import Thread
from .channel import PartialMessageable from .channel import PartialMessageable
from .interactions import Interaction from .interactions import Interaction
from .commands import MessageCommand from .commands import MessageCommand
from .abc import _handle_commands
if TYPE_CHECKING: if TYPE_CHECKING:
@ -1377,6 +1378,9 @@ class Message(PartialMessage, Hashable):
f'<{name} id={self.id} channel={self.channel!r} type={self.type!r} author={self.author!r} flags={self.flags!r}>' f'<{name} id={self.id} channel={self.channel!r} type={self.type!r} author={self.author!r} flags={self.flags!r}>'
) )
async def _get_channel(self) -> MessageableChannel:
return self.channel
def _try_patch(self, data, key, transform=None) -> None: def _try_patch(self, data, key, transform=None) -> None:
try: try:
value = data[key] value = data[key]
@ -1980,14 +1984,14 @@ class Message(PartialMessage, Hashable):
""" """
return await self.edit(attachments=[a for a in self.attachments if a not in attachments]) return await self.edit(attachments=[a for a in self.attachments if a not in attachments])
async def message_commands( def message_commands(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
*, *,
limit: Optional[int] = None, limit: Optional[int] = None,
command_ids: Optional[List[int]] = None, command_ids: Optional[List[int]] = None,
application: Optional[Snowflake] = None, application: Optional[Snowflake] = None,
include_applications: bool = MISSING, include_applications: bool = True,
) -> AsyncIterator[MessageCommand]: ) -> AsyncIterator[MessageCommand]:
"""Returns a :term:`asynchronous iterator` of the message commands available to use on the message. """Returns a :term:`asynchronous iterator` of the message commands available to use on the message.
@ -2013,23 +2017,24 @@ class Message(PartialMessage, Hashable):
This parameter is faked if ``application`` is specified. This parameter is faked if ``application`` is specified.
limit: Optional[:class:`int`] limit: Optional[:class:`int`]
The maximum number of commands to send back. Defaults to 25. If ``None``, returns all commands. The maximum number of commands to send back. Defaults to 0 if ``command_ids`` is passed, else 25.
If ``None``, returns all commands.
This parameter is faked if ``application`` is specified. This parameter is faked if ``application`` is specified.
command_ids: Optional[List[:class:`int`]] command_ids: Optional[List[:class:`int`]]
List of up to 100 command IDs to search for. If the command doesn't exist, it won't be returned. List of up to 100 command IDs to search for. If the command doesn't exist, it won't be returned.
If ``limit`` is passed alongside this parameter, this parameter will serve as a "preferred commands" list.
This means that the endpoint will return the found commands + ``limit`` more, if available.
application: Optional[:class:`~discord.abc.Snowflake`] application: Optional[:class:`~discord.abc.Snowflake`]
Return this application's commands. Always set to DM recipient in a private channel context. Whether to return this application's commands. Always set to DM recipient in a private channel context.
include_applications: :class:`bool` include_applications: :class:`bool`
Whether to include applications in the response. This defaults to ``True`` if possible. Whether to include applications in the response. Defaults to ``True``.
Cannot be set to ``True`` if ``application`` is specified.
Raises Raises
------ ------
TypeError TypeError
Both query and command_ids are passed. Both query and command_ids are passed.
Both application and include_applications are passed.
Attempted to fetch commands in a DM with a non-bot user. Attempted to fetch commands in a DM with a non-bot user.
ValueError ValueError
The limit was not greater than or equal to 0. The limit was not greater than or equal to 0.
@ -2045,64 +2050,13 @@ class Message(PartialMessage, Hashable):
:class:`.MessageCommand` :class:`.MessageCommand`
A message command. A message command.
""" """
if limit is not None and limit < 0: return _handle_commands(
raise ValueError('limit must be greater than or equal to 0') self,
if query and command_ids: AppCommandType.message,
raise TypeError('Cannot specify both query and command_ids') query=query,
limit=limit,
state = self._state command_ids=command_ids,
endpoint = state.http.search_application_commands application=application,
channel = self.channel include_applications=include_applications,
target=self,
application_id = application.id if application else None
if channel.type == ChannelType.private:
recipient: User = channel.recipient # type: ignore
if not recipient.bot:
raise TypeError('Cannot fetch commands in a DM with a non-bot user')
application_id = recipient.id
elif channel.type == ChannelType.group:
return
if application_id and include_applications:
raise TypeError('Cannot specify both application and include_applications')
include_applications = (
(False if application_id else True) if include_applications is MISSING else include_applications
)
while True:
cursor = MISSING
retrieve = min(25 if limit is None else limit, 25)
if retrieve < 1 or cursor is None:
return
data = await endpoint(
channel.id,
AppCommandType.message.value,
limit=retrieve if not application_id else None,
query=query if not command_ids and not application_id else None,
command_ids=command_ids if not application_id else None, # type: ignore
application_id=application_id,
include_applications=include_applications if not application_id else None,
cursor=cursor if cursor is not MISSING else None,
) )
cursor = data['cursor'].get('next')
cmds = data['application_commands']
apps: Dict[int, dict] = {int(app['id']): app for app in data.get('applications') or []}
if len(cmds) < 25:
limit = 0
for cmd in cmds:
# Handle faked parameters
if command_ids and int(cmd['id']) not in command_ids:
continue
elif application_id and query and query.lower() not in cmd['name'].lower():
continue
elif application_id and limit == 0:
return
if limit is not None:
limit -= 1
cmd['application'] = apps.get(int(cmd['application_id']))
yield MessageCommand(state=state, data=cmd, channel=channel, message=self)

2
discord/modal.py

@ -139,7 +139,7 @@ class Modal(Hashable):
i = await state.client.wait_for( i = await state.client.wait_for(
'interaction_finish', 'interaction_finish',
check=lambda d: d.nonce == nonce, check=lambda d: d.nonce == nonce,
timeout=7, timeout=6,
) )
except TimeoutError as exc: except TimeoutError as exc:
raise InvalidData('Did not receive a response from Discord') from exc raise InvalidData('Did not receive a response from Discord') from exc

75
docs/api.rst

@ -4071,11 +4071,17 @@ Messageable
.. autoclass:: discord.abc.Messageable() .. autoclass:: discord.abc.Messageable()
:members: :members:
:exclude-members: typing :exclude-members: typing, slash_commands, user_commands
.. automethod:: discord.abc.Messageable.typing .. automethod:: typing
:async-with: :async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
Connectable Connectable
~~~~~~~~~~~~ ~~~~~~~~~~~~
@ -4132,11 +4138,17 @@ User
.. autoclass:: User() .. autoclass:: User()
:members: :members:
:inherited-members: :inherited-members:
:exclude-members: typing :exclude-members: typing, slash_commands, user_commands
.. automethod:: typing .. automethod:: typing
:async-with: :async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: UserProfile .. attributetable:: UserProfile
.. autoclass:: UserProfile() .. autoclass:: UserProfile()
@ -4332,11 +4344,17 @@ Member
.. autoclass:: Member() .. autoclass:: Member()
:members: :members:
:inherited-members: :inherited-members:
:exclude-members: typing :exclude-members: typing, slash_commands, user_commands
.. automethod:: typing .. automethod:: typing
:async-with: :async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: MemberProfile .. attributetable:: MemberProfile
.. autoclass:: MemberProfile() .. autoclass:: MemberProfile()
@ -4408,21 +4426,33 @@ GuildChannel
.. autoclass:: TextChannel() .. autoclass:: TextChannel()
:members: :members:
:inherited-members: :inherited-members:
:exclude-members: typing :exclude-members: typing, slash_commands, user_commands
.. automethod:: typing .. automethod:: typing
:async-with: :async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: VoiceChannel .. attributetable:: VoiceChannel
.. autoclass:: VoiceChannel() .. autoclass:: VoiceChannel()
:members: :members:
:inherited-members: :inherited-members:
:exclude-members: typing :exclude-members: typing, slash_commands, user_commands
.. automethod:: typing .. automethod:: typing
:async-with: :async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: StageChannel .. attributetable:: StageChannel
.. autoclass:: StageChannel() .. autoclass:: StageChannel()
@ -4443,21 +4473,33 @@ PrivateChannel
.. autoclass:: DMChannel() .. autoclass:: DMChannel()
:members: :members:
:inherited-members: :inherited-members:
:exclude-members: typing :exclude-members: typing, slash_commands, user_commands
.. automethod:: typing .. automethod:: typing
:async-with: :async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: GroupChannel .. attributetable:: GroupChannel
.. autoclass:: GroupChannel() .. autoclass:: GroupChannel()
:members: :members:
:inherited-members: :inherited-members:
:exclude-members: typing :exclude-members: typing, slash_commands, user_commands
.. automethod:: typing .. automethod:: typing
:async-with: :async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
PartialMessageable PartialMessageable
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
@ -4475,11 +4517,17 @@ Thread
.. autoclass:: Thread() .. autoclass:: Thread()
:members: :members:
:inherited-members: :inherited-members:
:exclude-members: typing :exclude-members: typing, slash_commands, user_commands
.. automethod:: typing .. automethod:: typing
:async-with: :async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: ThreadMember .. attributetable:: ThreadMember
.. autoclass:: ThreadMember() .. autoclass:: ThreadMember()
@ -4520,6 +4568,10 @@ Message
.. autoclass:: Message() .. autoclass:: Message()
:members: :members:
:inherited-members: :inherited-members:
:exclude-members: message_commands
.. automethod:: message_commands
:async-for:
.. attributetable:: PartialMessage .. attributetable:: PartialMessage
@ -4604,11 +4656,6 @@ Component
ApplicationCommand ApplicationCommand
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
.. attributetable:: BaseCommand
.. autoclass:: BaseCommand()
:members:
.. attributetable:: UserCommand .. attributetable:: UserCommand
.. autoclass:: UserCommand() .. autoclass:: UserCommand()

13
docs/ext/commands/api.rst

@ -338,11 +338,20 @@ Context
.. autoclass:: discord.ext.commands.Context .. autoclass:: discord.ext.commands.Context
:members: :members:
:inherited-members: :inherited-members:
:exclude-members: typing :exclude-members: typing, slash_commands, user_commands, message_commands
.. automethod:: discord.ext.commands.Context.typing .. automethod:: typing
:async-with: :async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. automethod:: message_commands
:async-for:
.. _ext_commands_api_converters: .. _ext_commands_api_converters:
Converters Converters

Loading…
Cancel
Save