Browse Source

Implement commands v3

pull/10109/head
dolfies 1 year ago
parent
commit
8d77474269
  1. 172
      discord/abc.py
  2. 2
      discord/activity.py
  3. 2
      discord/commands.py
  4. 1
      discord/ext/commands/context.py
  5. 37
      discord/guild.py
  6. 9
      discord/http.py
  7. 13
      discord/message.py
  8. 9
      discord/types/command.py
  9. 5
      discord/utils.py

172
discord/abc.py

@ -170,7 +170,6 @@ def _handle_commands(
limit: Optional[int] = ..., limit: Optional[int] = ...,
command_ids: Optional[Collection[int]] = ..., command_ids: Optional[Collection[int]] = ...,
application: Optional[Snowflake] = ..., application: Optional[Snowflake] = ...,
with_applications: bool = ...,
target: Optional[Snowflake] = ..., target: Optional[Snowflake] = ...,
) -> AsyncIterator[SlashCommand]: ) -> AsyncIterator[SlashCommand]:
... ...
@ -185,7 +184,6 @@ def _handle_commands(
limit: Optional[int] = ..., limit: Optional[int] = ...,
command_ids: Optional[Collection[int]] = ..., command_ids: Optional[Collection[int]] = ...,
application: Optional[Snowflake] = ..., application: Optional[Snowflake] = ...,
with_applications: bool = ...,
target: Optional[Snowflake] = ..., target: Optional[Snowflake] = ...,
) -> AsyncIterator[UserCommand]: ) -> AsyncIterator[UserCommand]:
... ...
@ -200,7 +198,6 @@ def _handle_commands(
limit: Optional[int] = ..., limit: Optional[int] = ...,
command_ids: Optional[Collection[int]] = ..., command_ids: Optional[Collection[int]] = ...,
application: Optional[Snowflake] = ..., application: Optional[Snowflake] = ...,
with_applications: bool = ...,
target: Optional[Snowflake] = ..., target: Optional[Snowflake] = ...,
) -> AsyncIterator[MessageCommand]: ) -> AsyncIterator[MessageCommand]:
... ...
@ -208,13 +205,12 @@ def _handle_commands(
async def _handle_commands( async def _handle_commands(
messageable: Union[Messageable, Message], messageable: Union[Messageable, Message],
type: ApplicationCommandType, type: Optional[ApplicationCommandType] = None,
*, *,
query: Optional[str] = None, query: Optional[str] = None,
limit: Optional[int] = None, limit: Optional[int] = None,
command_ids: Optional[Collection[int]] = None, command_ids: Optional[Collection[int]] = None,
application: Optional[Snowflake] = None, application: Optional[Snowflake] = None,
with_applications: bool = True,
target: Optional[Snowflake] = None, target: Optional[Snowflake] = None,
) -> AsyncIterator[BaseCommand]: ) -> AsyncIterator[BaseCommand]:
if limit is not None and limit < 0: if limit is not None and limit < 0:
@ -222,70 +218,47 @@ async def _handle_commands(
if query and command_ids: if query and command_ids:
raise TypeError('Cannot specify both 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() channel = await messageable._get_channel()
_, cls = _command_factory(type.value)
cmd_ids = list(command_ids) if command_ids else None cmd_ids = list(command_ids) if command_ids else None
application_id = application.id if application else None application_id = application.id if application else None
if channel.type == ChannelType.private: if channel.type == ChannelType.private:
recipient: User = channel.recipient # type: ignore target = 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: elif channel.type == ChannelType.group:
return return
prev_cursor = MISSING cmds = await channel.application_commands()
cursor = MISSING
while True:
# We keep two cursors because Discord just sends us an infinite loop sometimes
retrieve = min((25 if not cmd_ids else 0) if limit is None else limit, 25)
if not application_id and limit is not None:
limit -= retrieve
if (not cmd_ids and retrieve < 1) or cursor is None or (prev_cursor is not MISSING and prev_cursor == cursor):
return
data = await endpoint( for cmd in cmds:
channel.id, # Handle faked parameters
type.value, if type is not None and cmd.type != type:
limit=retrieve if not application_id else None, continue
query=query if not cmd_ids and not application_id else None, if query and query.lower() not in cmd.name:
command_ids=cmd_ids if not application_id and not cursor else None, # type: ignore continue
application_id=application_id, if (not cmd_ids or cmd.id not in cmd_ids) and limit == 0:
include_applications=with_applications if (not application_id or with_applications) else None, continue
cursor=cursor, if application_id and cmd.application_id != application_id:
) continue
prev_cursor = cursor if target:
cursor = data['cursor'].get('next') if cmd.type == ApplicationCommandType.user:
cmds = data['application_commands'] cmd._user = target
apps = {int(app['id']): state.create_integration_application(app) for app in data.get('applications') or []} elif cmd.type == ApplicationCommandType.message:
cmd._message = target # type: ignore
for cmd in cmds:
# Handle faked parameters
if application_id and query and query.lower() not in cmd['name']:
continue
elif application_id and (not cmd_ids or int(cmd['id']) not in cmd_ids) and limit == 0:
continue
# We follow Discord behavior # We follow Discord behavior
if application_id and limit is not None and (not cmd_ids or int(cmd['id']) not in cmd_ids): if limit is not None and (not cmd_ids or cmd.id not in cmd_ids):
limit -= 1 limit -= 1
try: try:
cmd_ids.remove(int(cmd['id'])) if cmd_ids else None cmd_ids.remove(cmd.id) if cmd_ids else None
except ValueError: except ValueError:
pass pass
application = apps.get(int(cmd['application_id'])) yield cmd
yield cls(state=state, data=cmd, channel=channel, target=target, application=application)
cmd_ids = None cmd_ids = None
if application_id or len(cmds) < min(limit if limit else 25, 25) or len(cmds) == limit == 25: if len(cmds) < min(limit if limit else 25, 25) or len(cmds) == limit == 25:
return return
async def _handle_message_search( async def _handle_message_search(
@ -1406,9 +1379,9 @@ class GuildChannel:
An invalid position was given. An invalid position was given.
TypeError TypeError
A bad mix of arguments were passed. A bad mix of arguments were passed.
Forbidden ~discord.Forbidden
You do not have permissions to move the channel. You do not have permissions to move the channel.
HTTPException ~discord.HTTPException
Moving the channel failed. Moving the channel failed.
""" """
@ -2407,6 +2380,58 @@ class Messageable:
most_relevant=most_relevant, most_relevant=most_relevant,
) )
async def application_commands(self) -> List[Union[SlashCommand, UserCommand, MessageCommand]]:
"""|coro|
Returns a list of application commands available in the channel.
.. versionadded:: 2.1
.. note::
Commands that the user does not have permission to use will not be returned.
Raises
------
TypeError
Attempted to fetch commands in a DM with a non-bot user.
ValueError
Could not resolve the channel's guild ID.
~discord.HTTPException
Getting the commands failed.
Returns
-------
List[Union[:class:`~discord.SlashCommand`, :class:`~discord.UserCommand`, :class:`~discord.MessageCommand`]]
A list of application commands.
"""
channel = await self._get_channel()
state = self._state
if channel.type is ChannelType.private:
if not channel.recipient.bot: # type: ignore
raise TypeError('Cannot fetch commands in a DM with a non-bot user')
data = await state.http.channel_application_command_index(channel.id)
elif channel.type is ChannelType.group:
# TODO: Are commands in group DMs truly dead?
return []
else:
guild_id = getattr(channel.guild, 'id', getattr(channel, 'guild_id', None))
if not guild_id:
raise ValueError('Could not resolve channel guild ID') from None
data = await state.http.guild_application_command_index(guild_id)
cmds = data['application_commands']
apps = {int(app['id']): state.create_integration_application(app) for app in data.get('applications') or []}
result = []
for cmd in cmds:
_, cls = _command_factory(cmd['type'])
application = apps.get(int(cmd['application_id']))
result.append(cls(state=state, data=cmd, channel=channel, application=application))
return result
@utils.deprecated('Messageable.application_commands')
def slash_commands( def slash_commands(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
@ -2418,6 +2443,8 @@ class Messageable:
) -> 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.
.. deprecated:: 2.1
Examples Examples
--------- ---------
@ -2437,13 +2464,8 @@ class Messageable:
---------- ----------
query: Optional[:class:`str`] query: Optional[:class:`str`]
The query to search for. Specifying this limits results to 25 commands max. The query to search for. Specifying this limits results to 25 commands max.
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 0 if ``command_ids`` is passed, else 25. The maximum number of commands to send back. If ``None``, returns all commands.
If ``None``, returns all commands.
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.
@ -2452,7 +2474,7 @@ class Messageable:
application: Optional[:class:`~discord.abc.Snowflake`] application: Optional[:class:`~discord.abc.Snowflake`]
Whether to 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.
with_applications: :class:`bool` with_applications: :class:`bool`
Whether to include applications in the response. Defaults to ``True``. Whether to include applications in the response.
Raises Raises
------ ------
@ -2461,7 +2483,8 @@ class Messageable:
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.
HTTPException Could not resolve the channel's guild ID.
~discord.HTTPException
Getting the commands failed. Getting the commands failed.
~discord.Forbidden ~discord.Forbidden
You do not have permissions to get the commands. You do not have permissions to get the commands.
@ -2480,9 +2503,9 @@ class Messageable:
limit=limit, limit=limit,
command_ids=command_ids, command_ids=command_ids,
application=application, application=application,
with_applications=with_applications,
) )
@utils.deprecated('Messageable.application_commands')
def user_commands( def user_commands(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
@ -2494,6 +2517,8 @@ class Messageable:
) -> 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.
.. deprecated:: 2.1
Examples Examples
--------- ---------
@ -2513,13 +2538,8 @@ class Messageable:
---------- ----------
query: Optional[:class:`str`] query: Optional[:class:`str`]
The query to search for. Specifying this limits results to 25 commands max. The query to search for. Specifying this limits results to 25 commands max.
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 0 if ``command_ids`` is passed, else 25. The maximum number of commands to send back. If ``None``, returns all commands.
If ``None``, returns all commands.
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.
@ -2528,7 +2548,7 @@ class Messageable:
application: Optional[:class:`~discord.abc.Snowflake`] application: Optional[:class:`~discord.abc.Snowflake`]
Whether to 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.
with_applications: :class:`bool` with_applications: :class:`bool`
Whether to include applications in the response. Defaults to ``True``. Whether to include applications in the response.
Raises Raises
------ ------
@ -2537,7 +2557,8 @@ class Messageable:
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.
HTTPException Could not resolve the channel's guild ID.
~discord.HTTPException
Getting the commands failed. Getting the commands failed.
~discord.Forbidden ~discord.Forbidden
You do not have permissions to get the commands. You do not have permissions to get the commands.
@ -2556,7 +2577,6 @@ class Messageable:
limit=limit, limit=limit,
command_ids=command_ids, command_ids=command_ids,
application=application, application=application,
with_applications=with_applications,
) )

2
discord/activity.py

@ -211,7 +211,7 @@ class Activity(BaseActivity):
'application_id', 'application_id',
'emoji', 'emoji',
'buttons', 'buttons',
'metadata' 'metadata',
) )
def __init__(self, **kwargs: Any) -> None: def __init__(self, **kwargs: Any) -> None:

2
discord/commands.py

@ -219,7 +219,7 @@ class BaseCommand(ApplicationCommand, Hashable):
self._data = data self._data = data
self.application = application self.application = application
self.name = data['name'] self.name = data['name']
self.description = data['description'] self.description = data.get('description', '')
self._channel = channel self._channel = channel
self.application_id: int = int(data['application_id']) self.application_id: int = int(data['application_id'])
self.id: int = int(data['id']) self.id: int = int(data['id'])

1
discord/ext/commands/context.py

@ -516,6 +516,7 @@ 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)
@discord.utils.deprecated("Context.application_commands")
@discord.utils.copy_doc(Message.message_commands) @discord.utils.copy_doc(Message.message_commands)
def message_commands( def message_commands(
self, self,

37
discord/guild.py

@ -100,6 +100,7 @@ from .guild_premium import PremiumGuildSubscription
from .entitlements import Entitlement from .entitlements import Entitlement
from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction
from .partial_emoji import _EmojiTag, PartialEmoji from .partial_emoji import _EmojiTag, PartialEmoji
from .commands import _command_factory
if TYPE_CHECKING: if TYPE_CHECKING:
from .abc import Snowflake, SnowflakeTime from .abc import Snowflake, SnowflakeTime
@ -140,6 +141,7 @@ if TYPE_CHECKING:
from .types.oauth2 import OAuth2Guild as OAuth2GuildPayload from .types.oauth2 import OAuth2Guild as OAuth2GuildPayload
from .message import EmojiInputType, Message from .message import EmojiInputType, Message
from .read_state import ReadState from .read_state import ReadState
from .commands import UserCommand, MessageCommand, SlashCommand
VocalGuildChannel = Union[VoiceChannel, StageChannel] VocalGuildChannel = Union[VoiceChannel, StageChannel]
NonCategoryChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, DirectoryChannel] NonCategoryChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, DirectoryChannel]
@ -1318,7 +1320,7 @@ class Guild(Hashable):
@property @property
def application_command_count(self) -> Optional[int]: def application_command_count(self) -> Optional[int]:
"""Optional[:class:`int`]: Returns the application command count if available. """Optional[:class:`int`]: Returns the application command count, if available.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
@ -3273,6 +3275,39 @@ class Guild(Hashable):
return [convert(d) for d in data] return [convert(d) for d in data]
async def application_commands(self) -> List[Union[SlashCommand, UserCommand, MessageCommand]]:
"""|coro|
Returns a list of all application commands available in the guild.
.. versionadded:: 2.1
.. note::
Commands that the user does not have permission to use will not be returned.
Raises
-------
HTTPException
Fetching the commands failed.
Returns
--------
List[Union[:class:`SlashCommand`, :class:`UserCommand`, :class:`MessageCommand`]]
The list of application commands that are available in the guild.
"""
state = self._state
data = await state.http.guild_application_command_index(self.id)
cmds = data['application_commands']
apps = {int(app['id']): state.create_integration_application(app) for app in data.get('applications') or []}
result = []
for cmd in cmds:
_, cls = _command_factory(cmd['type'])
application = apps.get(int(cmd['application_id']))
result.append(cls(state=state, data=cmd, application=application))
return result
async def fetch_stickers(self) -> List[GuildSticker]: async def fetch_stickers(self) -> List[GuildSticker]:
r"""|coro| r"""|coro|

9
discord/http.py

@ -4451,6 +4451,15 @@ class HTTPClient:
Route('GET', '/channels/{channel_id}/application-commands/search', channel_id=channel_id), params=params Route('GET', '/channels/{channel_id}/application-commands/search', channel_id=channel_id), params=params
) )
def guild_application_command_index(self, guild_id: Snowflake) -> Response[command.GuildApplicationCommandIndex]:
return self.request(Route('GET', '/guilds/{guild_id}/application-command-index', guild_id=guild_id))
def channel_application_command_index(self, channel_id: Snowflake) -> Response[command.ApplicationCommandIndex]:
return self.request(Route('GET', '/channels/{channel_id}/application-command-index', channel_id=channel_id))
def user_application_command_index(self) -> Response[command.ApplicationCommandIndex]:
return self.request(Route('GET', '/users/@me/application-command-index'))
def interact( def interact(
self, self,
type: InteractionType, type: InteractionType,

13
discord/message.py

@ -2338,6 +2338,7 @@ 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])
@utils.deprecated("Message.channel.application_commands")
def message_commands( def message_commands(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
@ -2349,6 +2350,8 @@ class Message(PartialMessage, Hashable):
) -> 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.
.. deprecated:: 2.1
Examples Examples
--------- ---------
@ -2368,13 +2371,8 @@ class Message(PartialMessage, Hashable):
---------- ----------
query: Optional[:class:`str`] query: Optional[:class:`str`]
The query to search for. Specifying this limits results to 25 commands max. The query to search for. Specifying this limits results to 25 commands max.
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 0 if ``command_ids`` is passed, else 25. The maximum number of commands to send back. If ``None``, returns all commands.
If ``None``, returns all commands.
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.
@ -2383,7 +2381,7 @@ class Message(PartialMessage, Hashable):
application: Optional[:class:`~discord.abc.Snowflake`] application: Optional[:class:`~discord.abc.Snowflake`]
Whether to 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.
with_applications: :class:`bool` with_applications: :class:`bool`
Whether to include applications in the response. Defaults to ``True``. Whether to include applications in the response.
Raises Raises
------ ------
@ -2411,6 +2409,5 @@ class Message(PartialMessage, Hashable):
limit=limit, limit=limit,
command_ids=command_ids, command_ids=command_ids,
application=application, application=application,
with_applications=with_applications,
target=self, target=self,
) )

9
discord/types/command.py

@ -222,7 +222,14 @@ class ApplicationCommandCursor(TypedDict):
repaired: Optional[str] repaired: Optional[str]
class ApplicationCommandSearch(TypedDict): class ApplicationCommandIndex(TypedDict):
application_commands: List[ApplicationCommand] application_commands: List[ApplicationCommand]
applications: Optional[List[IntegrationApplication]] applications: Optional[List[IntegrationApplication]]
class GuildApplicationCommandIndex(ApplicationCommandIndex):
version: Snowflake
class ApplicationCommandSearch(ApplicationCommandIndex):
cursor: ApplicationCommandCursor cursor: ApplicationCommandCursor

5
discord/utils.py

@ -1354,6 +1354,7 @@ def format_dt(dt: datetime.datetime, /, style: Optional[TimestampStyle] = None)
return f'<t:{int(dt.timestamp())}:{style}>' return f'<t:{int(dt.timestamp())}:{style}>'
@deprecated()
def set_target( def set_target(
items: Iterable[ApplicationCommand], items: Iterable[ApplicationCommand],
*, *,
@ -1368,6 +1369,10 @@ def set_target(
Suppresses all AttributeErrors so you can pass multiple types of commands and Suppresses all AttributeErrors so you can pass multiple types of commands and
not worry about which elements support which parameter. not worry about which elements support which parameter.
.. versionadded:: 2.0
.. deprecated:: 2.1
Parameters Parameters
----------- -----------
items: Iterable[:class:`.abc.ApplicationCommand`] items: Iterable[:class:`.abc.ApplicationCommand`]

Loading…
Cancel
Save