diff --git a/discord/abc.py b/discord/abc.py index 962e0a0383bc..2263facf124c 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -170,7 +170,6 @@ def _handle_commands( limit: Optional[int] = ..., command_ids: Optional[Collection[int]] = ..., application: Optional[Snowflake] = ..., - with_applications: bool = ..., target: Optional[Snowflake] = ..., ) -> AsyncIterator[SlashCommand]: ... @@ -185,7 +184,6 @@ def _handle_commands( limit: Optional[int] = ..., command_ids: Optional[Collection[int]] = ..., application: Optional[Snowflake] = ..., - with_applications: bool = ..., target: Optional[Snowflake] = ..., ) -> AsyncIterator[UserCommand]: ... @@ -200,7 +198,6 @@ def _handle_commands( limit: Optional[int] = ..., command_ids: Optional[Collection[int]] = ..., application: Optional[Snowflake] = ..., - with_applications: bool = ..., target: Optional[Snowflake] = ..., ) -> AsyncIterator[MessageCommand]: ... @@ -208,13 +205,12 @@ def _handle_commands( async def _handle_commands( messageable: Union[Messageable, Message], - type: ApplicationCommandType, + type: Optional[ApplicationCommandType] = None, *, query: Optional[str] = None, limit: Optional[int] = None, command_ids: Optional[Collection[int]] = None, application: Optional[Snowflake] = None, - with_applications: bool = True, target: Optional[Snowflake] = None, ) -> AsyncIterator[BaseCommand]: if limit is not None and limit < 0: @@ -222,70 +218,47 @@ async def _handle_commands( 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() - _, cls = _command_factory(type.value) cmd_ids = list(command_ids) if command_ids else None 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 + target = channel.recipient # type: ignore elif channel.type == ChannelType.group: return - prev_cursor = MISSING - 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 + cmds = await channel.application_commands() - data = await endpoint( - channel.id, - type.value, - limit=retrieve if not application_id else None, - query=query if not cmd_ids and not application_id else None, - command_ids=cmd_ids if not application_id and not cursor else None, # type: ignore - application_id=application_id, - include_applications=with_applications if (not application_id or with_applications) else None, - cursor=cursor, - ) - prev_cursor = cursor - cursor = data['cursor'].get('next') - cmds = data['application_commands'] - apps = {int(app['id']): state.create_integration_application(app) for app in data.get('applications') or []} - - 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 + for cmd in cmds: + # Handle faked parameters + if type is not None and cmd.type != type: + continue + if query and query.lower() not in cmd.name: + continue + if (not cmd_ids or cmd.id not in cmd_ids) and limit == 0: + continue + if application_id and cmd.application_id != application_id: + continue + if target: + if cmd.type == ApplicationCommandType.user: + cmd._user = target + elif cmd.type == ApplicationCommandType.message: + cmd._message = target # type: ignore - # We follow Discord behavior - if application_id and limit is not None and (not cmd_ids or int(cmd['id']) not in cmd_ids): - limit -= 1 + # We follow Discord behavior + if limit is not None and (not cmd_ids or cmd.id not in cmd_ids): + limit -= 1 - try: - cmd_ids.remove(int(cmd['id'])) if cmd_ids else None - except ValueError: - pass + try: + cmd_ids.remove(cmd.id) if cmd_ids else None + except ValueError: + pass - application = apps.get(int(cmd['application_id'])) - yield cls(state=state, data=cmd, channel=channel, target=target, application=application) + yield cmd - cmd_ids = None - if application_id or len(cmds) < min(limit if limit else 25, 25) or len(cmds) == limit == 25: - return + cmd_ids = None + if len(cmds) < min(limit if limit else 25, 25) or len(cmds) == limit == 25: + return async def _handle_message_search( @@ -1406,9 +1379,9 @@ async def move(self, **kwargs: Any) -> None: An invalid position was given. TypeError A bad mix of arguments were passed. - Forbidden + ~discord.Forbidden You do not have permissions to move the channel. - HTTPException + ~discord.HTTPException Moving the channel failed. """ @@ -2407,6 +2380,58 @@ def search( 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( self, query: Optional[str] = None, @@ -2418,6 +2443,8 @@ def slash_commands( ) -> AsyncIterator[SlashCommand]: """Returns a :term:`asynchronous iterator` of the slash commands available in the channel. + .. deprecated:: 2.1 + Examples --------- @@ -2437,13 +2464,8 @@ def slash_commands( ---------- query: Optional[:class:`str`] 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`] - 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. + The maximum number of commands to send back. If ``None``, returns all commands. 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. @@ -2452,7 +2474,7 @@ def slash_commands( application: Optional[:class:`~discord.abc.Snowflake`] Whether to return this application's commands. Always set to DM recipient in a private channel context. with_applications: :class:`bool` - Whether to include applications in the response. Defaults to ``True``. + Whether to include applications in the response. Raises ------ @@ -2461,7 +2483,8 @@ def slash_commands( Attempted to fetch commands in a DM with a non-bot user. ValueError 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. ~discord.Forbidden You do not have permissions to get the commands. @@ -2480,9 +2503,9 @@ def slash_commands( limit=limit, command_ids=command_ids, application=application, - with_applications=with_applications, ) + @utils.deprecated('Messageable.application_commands') def user_commands( self, query: Optional[str] = None, @@ -2494,6 +2517,8 @@ def user_commands( ) -> AsyncIterator[UserCommand]: """Returns a :term:`asynchronous iterator` of the user commands available to use on the user. + .. deprecated:: 2.1 + Examples --------- @@ -2513,13 +2538,8 @@ def user_commands( ---------- query: Optional[:class:`str`] 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`] - 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. + The maximum number of commands to send back. If ``None``, returns all commands. 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. @@ -2528,7 +2548,7 @@ def user_commands( application: Optional[:class:`~discord.abc.Snowflake`] Whether to return this application's commands. Always set to DM recipient in a private channel context. with_applications: :class:`bool` - Whether to include applications in the response. Defaults to ``True``. + Whether to include applications in the response. Raises ------ @@ -2537,7 +2557,8 @@ def user_commands( Attempted to fetch commands in a DM with a non-bot user. ValueError 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. ~discord.Forbidden You do not have permissions to get the commands. @@ -2556,7 +2577,6 @@ def user_commands( limit=limit, command_ids=command_ids, application=application, - with_applications=with_applications, ) diff --git a/discord/activity.py b/discord/activity.py index 74be28d18816..353958506783 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -211,7 +211,7 @@ class Activity(BaseActivity): 'application_id', 'emoji', 'buttons', - 'metadata' + 'metadata', ) def __init__(self, **kwargs: Any) -> None: diff --git a/discord/commands.py b/discord/commands.py index 68200a7d4751..6b56095e4f59 100644 --- a/discord/commands.py +++ b/discord/commands.py @@ -219,7 +219,7 @@ def __init__( self._data = data self.application = application self.name = data['name'] - self.description = data['description'] + self.description = data.get('description', '') self._channel = channel self.application_id: int = int(data['application_id']) self.id: int = int(data['id']) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 0b19dafdb4e3..23386a0fe134 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -516,6 +516,7 @@ async def reply( async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: return await self.message.reply(content, **kwargs) + @discord.utils.deprecated("Context.application_commands") @discord.utils.copy_doc(Message.message_commands) def message_commands( self, diff --git a/discord/guild.py b/discord/guild.py index 00f746749c2b..3100449655d3 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -100,6 +100,7 @@ from .entitlements import Entitlement from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji +from .commands import _command_factory if TYPE_CHECKING: from .abc import Snowflake, SnowflakeTime @@ -140,6 +141,7 @@ from .types.oauth2 import OAuth2Guild as OAuth2GuildPayload from .message import EmojiInputType, Message from .read_state import ReadState + from .commands import UserCommand, MessageCommand, SlashCommand VocalGuildChannel = Union[VoiceChannel, StageChannel] NonCategoryChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, DirectoryChannel] @@ -1318,7 +1320,7 @@ def online_count(self) -> Optional[int]: @property 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 """ @@ -3273,6 +3275,39 @@ def convert(d): 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]: r"""|coro| diff --git a/discord/http.py b/discord/http.py index ac6dabd3b1e4..a03ca96b1cee 100644 --- a/discord/http.py +++ b/discord/http.py @@ -4451,6 +4451,15 @@ def search_application_commands( 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( self, type: InteractionType, diff --git a/discord/message.py b/discord/message.py index 88fdae2266e7..250d8ba2b4dd 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2338,6 +2338,7 @@ async def remove_attachments(self, *attachments: Attachment) -> Message: """ 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( self, query: Optional[str] = None, @@ -2349,6 +2350,8 @@ def message_commands( ) -> AsyncIterator[MessageCommand]: """Returns a :term:`asynchronous iterator` of the message commands available to use on the message. + .. deprecated:: 2.1 + Examples --------- @@ -2368,13 +2371,8 @@ def message_commands( ---------- query: Optional[:class:`str`] 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`] - 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. + The maximum number of commands to send back. If ``None``, returns all commands. 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. @@ -2383,7 +2381,7 @@ def message_commands( application: Optional[:class:`~discord.abc.Snowflake`] Whether to return this application's commands. Always set to DM recipient in a private channel context. with_applications: :class:`bool` - Whether to include applications in the response. Defaults to ``True``. + Whether to include applications in the response. Raises ------ @@ -2411,6 +2409,5 @@ def message_commands( limit=limit, command_ids=command_ids, application=application, - with_applications=with_applications, target=self, ) diff --git a/discord/types/command.py b/discord/types/command.py index 1569c0e9873d..251f899eb3d3 100644 --- a/discord/types/command.py +++ b/discord/types/command.py @@ -222,7 +222,14 @@ class ApplicationCommandCursor(TypedDict): repaired: Optional[str] -class ApplicationCommandSearch(TypedDict): +class ApplicationCommandIndex(TypedDict): application_commands: List[ApplicationCommand] applications: Optional[List[IntegrationApplication]] + + +class GuildApplicationCommandIndex(ApplicationCommandIndex): + version: Snowflake + + +class ApplicationCommandSearch(ApplicationCommandIndex): cursor: ApplicationCommandCursor diff --git a/discord/utils.py b/discord/utils.py index 5696546bfd42..b8bbad80561b 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1354,6 +1354,7 @@ def format_dt(dt: datetime.datetime, /, style: Optional[TimestampStyle] = None) return f'' +@deprecated() def set_target( items: Iterable[ApplicationCommand], *, @@ -1368,6 +1369,10 @@ def set_target( Suppresses all AttributeErrors so you can pass multiple types of commands and not worry about which elements support which parameter. + .. versionadded:: 2.0 + + .. deprecated:: 2.1 + Parameters ----------- items: Iterable[:class:`.abc.ApplicationCommand`]