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,
Dict,
List,
Literal,
Optional,
TYPE_CHECKING,
Protocol,
@ -57,7 +58,7 @@ from .http import handle_message_parameters
from .voice_client import VoiceClient, VoiceProtocol
from .sticker import GuildSticker, StickerItem
from .settings import ChannelSettings
from .commands import ApplicationCommand, SlashCommand, UserCommand
from .commands import ApplicationCommand, BaseCommand, SlashCommand, UserCommand, MessageCommand, _command_factory
from . import utils
__all__ = (
@ -145,6 +146,127 @@ async def _purge_helper(
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
class Snowflake(Protocol):
"""An ABC that details the common operations on a Discord model.
@ -194,6 +316,7 @@ class User(Snowflake, Protocol):
name: str
discriminator: str
bot: bool
system: bool
@property
def display_name(self) -> str:
@ -1529,7 +1652,7 @@ class Messageable:
async def ack_pins(self) -> None:
"""|coro|
Acks the channel's pins.
Marks a channel's pins as viewed.
Raises
-------
@ -1727,14 +1850,14 @@ class Messageable:
for raw_message in data:
yield self._state.create_message(channel=channel, data=raw_message)
async def slash_commands(
def slash_commands(
self,
query: Optional[str] = None,
*,
limit: Optional[int] = None,
command_ids: Optional[List[int]] = None,
application: Optional[Snowflake] = None,
include_applications: bool = MISSING,
include_applications: bool = True,
) -> AsyncIterator[SlashCommand]:
"""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.
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.
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.
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`]
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`
Whether to include applications in the response. This defaults to ``True`` if possible.
Cannot be set to ``True`` if ``application`` is specified.
Whether to include applications in the response. Defaults to ``True``.
Raises
------
TypeError
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.
ValueError
The limit was not greater than or equal to 0.
@ -1792,76 +1916,24 @@ class Messageable:
:class:`.SlashCommand`
A slash command.
"""
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 = self._state
endpoint = state.http.search_application_commands
channel = await self._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
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
return _handle_commands(
self,
AppCommandType.chat_input,
query=query,
limit=limit,
command_ids=command_ids,
application=application,
include_applications=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:
# 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(
def user_commands(
self,
query: Optional[str] = None,
*,
limit: Optional[int] = None,
command_ids: Optional[List[int]] = None,
application: Optional[Snowflake] = None,
include_applications: bool = MISSING,
include_applications: bool = True,
) -> AsyncIterator[UserCommand]:
"""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.
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.
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.
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`]
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`
Whether to include applications in the response. This defaults to ``True`` if possible.
Cannot be set to ``True`` if ``application`` is specified.
Whether to include applications in the response. Defaults to ``True``.
Raises
------
TypeError
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.
ValueError
The limit was not greater than or equal to 0.
@ -1919,68 +1992,16 @@ class Messageable:
:class:`.UserCommand`
A user command.
"""
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 = self._state
endpoint = state.http.search_application_commands
channel = await self._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
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
return _handle_commands(
self,
AppCommandType.user,
query=query,
limit=limit,
command_ids=command_ids,
application=application,
include_applications=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):
"""An ABC that details the common operations on a channel that can

282
discord/commands.py

@ -36,10 +36,12 @@ if TYPE_CHECKING:
from .abc import Messageable, Snowflake
from .appinfo import InteractionApplication
from .file import File
from .guild import Guild
from .interactions import Interaction
from .message import Attachment, Message
from .state import ConnectionState
__all__ = (
'BaseCommand',
'UserCommand',
@ -57,12 +59,13 @@ class ApplicationCommand(Protocol):
The following implement this ABC:
- :class:`~discord.BaseCommand`
- :class:`~discord.UserCommand`
- :class:`~discord.MessageCommand`
- :class:`~discord.SlashCommand`
- :class:`~discord.SubCommand`
.. versionadded:: 2.0
Attributes
-----------
name: :class:`str`
@ -75,11 +78,16 @@ class ApplicationCommand(Protocol):
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:`~discord.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.
"""
__slots__ = ()
@ -94,8 +102,11 @@ class ApplicationCommand(Protocol):
type: AppCommandType
default_permission: bool
dm_permission: bool
nsfw: bool
application_id: int
application: Optional[InteractionApplication]
mention: str
guild_id: Optional[int]
def __str__(self) -> str:
return self.name
@ -117,7 +128,7 @@ class ApplicationCommand(Protocol):
i = await state.client.wait_for(
'interaction_finish',
check=lambda d: d.nonce == nonce,
timeout=7,
timeout=6,
)
except TimeoutError as 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)
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:
"""Query whether this command is a group.
@ -163,49 +181,6 @@ class ApplicationCommand(Protocol):
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__ = (
'name',
'description',
@ -216,13 +191,15 @@ class BaseCommand(ApplicationCommand, Hashable):
'application',
'application_id',
'dm_permission',
'nsfw',
'guild_id',
'_data',
'_state',
'_channel',
'_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._data = data
self.name = data['name']
@ -240,10 +217,17 @@ class BaseCommand(ApplicationCommand, Hashable):
self.default_permission: bool = data.get('default_permission', True)
dm_permission = data.get('dm_permission') # Null means 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:
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):
if TYPE_CHECKING:
@ -270,6 +254,8 @@ class SlashMixin(ApplicationCommand, Protocol):
'type': obj.type.value,
'version': str(obj.version),
}
if self.guild_id:
data['guild_id'] = str(self.guild_id)
return await super().__call__(data, files, channel)
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):
"""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',)
def __init__(self, *, user: Optional[Snowflake] = None, **kwargs):
def __init__(self, *, target: Optional[Snowflake] = None, **kwargs):
super().__init__(**kwargs)
self._user = user
self._user = target
async def __call__(self, user: Optional[Snowflake] = None, *, channel: Optional[Messageable] = None):
"""Use the user command.
@ -385,13 +421,63 @@ class UserCommand(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',)
def __init__(self, *, message: Optional[Message] = None, **kwargs):
def __init__(self, *, target: Optional[Message] = None, **kwargs):
super().__init__(**kwargs)
self._message = message
self._message = target
async def __call__(self, message: Optional[Message] = None, *, channel: Optional[Messageable] = None):
"""Use the message command.
@ -443,13 +529,58 @@ class MessageCommand(BaseCommand):
class SlashCommand(BaseCommand, SlashMixin):
"""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
----------
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`]
The command's options.
children: List[:class:`SubCommand`]
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')
@ -503,6 +634,8 @@ class SlashCommand(BaseCommand, SlashMixin):
class SubCommand(SlashMixin):
"""Represents a slash command child.
.. versionadded:: 2.0
This could be a subcommand, or a subgroup.
.. container:: operations
@ -519,13 +652,14 @@ class SubCommand(SlashMixin):
The subcommand's description, if any.
type: :class:`AppCommandType`
The type of application command. Always :attr:`AppCommandType.chat_input`.
parent: :class:`SlashCommand`
parent: Union[:class:`SlashCommand`, :class:`SubCommand`]
The parent command.
options: List[:class:`Option`]
The subcommand's options.
children: List[:class:`SubCommand`]
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__ = (
@ -608,6 +742,22 @@ class SubCommand(SlashMixin):
BASE += f' children={len(self.children)}'
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
def _default_member_permissions(self) -> Optional[int]:
return self._parent._default_member_permissions
@ -632,6 +782,24 @@ class SubCommand(SlashMixin):
""":class:`bool`: Whether the command is enabled in DMs."""
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:
"""Query whether this command is a group.
@ -665,6 +833,8 @@ class SubCommand(SlashMixin):
class Option:
"""Represents a command option.
.. versionadded:: 2.0
.. container:: operations
.. describe:: str(x)
@ -739,6 +909,8 @@ class Option:
class OptionChoice:
"""Represents a choice for an option.
.. versionadded:: 2.0
.. container:: operations
.. describe:: str(x)

4
discord/components.py

@ -228,7 +228,7 @@ class Button(Component):
i = await state.client.wait_for(
'interaction_finish',
check=lambda d: d.nonce == nonce,
timeout=7,
timeout=6,
)
except TimeoutError as 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(
'interaction_finish',
check=lambda d: d.nonce == nonce,
timeout=7,
timeout=6,
)
except TimeoutError as 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:
return await self.message.reply(content, **kwargs)
async def message_commands(
@discord.utils.copy_doc(Message.message_commands)
def message_commands(
self,
query: Optional[str] = None,
*,
@ -420,7 +421,6 @@ class Context(discord.abc.Messageable, Generic[BotT]):
application: Optional[discord.abc.Snowflake] = None,
include_applications: bool = True,
) -> 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
):
yield command
)

2
discord/http.py

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

90
discord/message.py

@ -66,6 +66,7 @@ from .threads import Thread
from .channel import PartialMessageable
from .interactions import Interaction
from .commands import MessageCommand
from .abc import _handle_commands
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}>'
)
async def _get_channel(self) -> MessageableChannel:
return self.channel
def _try_patch(self, data, key, transform=None) -> None:
try:
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])
async def message_commands(
def message_commands(
self,
query: Optional[str] = None,
*,
limit: Optional[int] = None,
command_ids: Optional[List[int]] = None,
application: Optional[Snowflake] = None,
include_applications: bool = MISSING,
include_applications: bool = True,
) -> AsyncIterator[MessageCommand]:
"""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.
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.
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.
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`]
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`
Whether to include applications in the response. This defaults to ``True`` if possible.
Cannot be set to ``True`` if ``application`` is specified.
Whether to include applications in the response. Defaults to ``True``.
Raises
------
TypeError
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.
ValueError
The limit was not greater than or equal to 0.
@ -2045,64 +2050,13 @@ class Message(PartialMessage, Hashable):
:class:`.MessageCommand`
A message command.
"""
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 = self._state
endpoint = state.http.search_application_commands
channel = self.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
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
return _handle_commands(
self,
AppCommandType.message,
query=query,
limit=limit,
command_ids=command_ids,
application=application,
include_applications=include_applications,
target=self,
)
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(
'interaction_finish',
check=lambda d: d.nonce == nonce,
timeout=7,
timeout=6,
)
except TimeoutError as 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()
:members:
:exclude-members: typing
:exclude-members: typing, slash_commands, user_commands
.. automethod:: discord.abc.Messageable.typing
.. automethod:: typing
:async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
Connectable
~~~~~~~~~~~~
@ -4132,11 +4138,17 @@ User
.. autoclass:: User()
:members:
:inherited-members:
:exclude-members: typing
:exclude-members: typing, slash_commands, user_commands
.. automethod:: typing
:async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: UserProfile
.. autoclass:: UserProfile()
@ -4332,11 +4344,17 @@ Member
.. autoclass:: Member()
:members:
:inherited-members:
:exclude-members: typing
:exclude-members: typing, slash_commands, user_commands
.. automethod:: typing
:async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: MemberProfile
.. autoclass:: MemberProfile()
@ -4408,21 +4426,33 @@ GuildChannel
.. autoclass:: TextChannel()
:members:
:inherited-members:
:exclude-members: typing
:exclude-members: typing, slash_commands, user_commands
.. automethod:: typing
:async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: VoiceChannel
.. autoclass:: VoiceChannel()
:members:
:inherited-members:
:exclude-members: typing
:exclude-members: typing, slash_commands, user_commands
.. automethod:: typing
:async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: StageChannel
.. autoclass:: StageChannel()
@ -4443,21 +4473,33 @@ PrivateChannel
.. autoclass:: DMChannel()
:members:
:inherited-members:
:exclude-members: typing
:exclude-members: typing, slash_commands, user_commands
.. automethod:: typing
:async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: GroupChannel
.. autoclass:: GroupChannel()
:members:
:inherited-members:
:exclude-members: typing
:exclude-members: typing, slash_commands, user_commands
.. automethod:: typing
:async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
PartialMessageable
~~~~~~~~~~~~~~~~~~~
@ -4475,11 +4517,17 @@ Thread
.. autoclass:: Thread()
:members:
:inherited-members:
:exclude-members: typing
:exclude-members: typing, slash_commands, user_commands
.. automethod:: typing
:async-with:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. attributetable:: ThreadMember
.. autoclass:: ThreadMember()
@ -4520,6 +4568,10 @@ Message
.. autoclass:: Message()
:members:
:inherited-members:
:exclude-members: message_commands
.. automethod:: message_commands
:async-for:
.. attributetable:: PartialMessage
@ -4604,11 +4656,6 @@ Component
ApplicationCommand
~~~~~~~~~~~~~~~~~~
.. attributetable:: BaseCommand
.. autoclass:: BaseCommand()
:members:
.. attributetable:: UserCommand
.. autoclass:: UserCommand()

13
docs/ext/commands/api.rst

@ -338,11 +338,20 @@ Context
.. autoclass:: discord.ext.commands.Context
: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:
.. automethod:: slash_commands
:async-for:
.. automethod:: user_commands
:async-for:
.. automethod:: message_commands
:async-for:
.. _ext_commands_api_converters:
Converters

Loading…
Cancel
Save