diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index 87dabd83c..3fb75e21d 100644 --- a/discord/app_commands/errors.py +++ b/discord/app_commands/errors.py @@ -45,6 +45,7 @@ __all__ = ( 'MissingPermissions', 'BotMissingPermissions', 'CommandOnCooldown', + 'MissingApplicationID', ) if TYPE_CHECKING: @@ -53,6 +54,11 @@ if TYPE_CHECKING: from ..types.snowflake import Snowflake, SnowflakeList from .checks import Cooldown +APP_ID_NOT_FOUND = ( + 'Client does not have an application_id set. Either the function was called before on_ready ' + 'was called or application_id was not passed to the Client constructor.' +) + class AppCommandError(DiscordException): """The base exception type for all application command related errors. @@ -391,3 +397,16 @@ class CommandSignatureMismatch(AppCommandError): 'command tree to fix this issue.' ) super().__init__(msg) + + +class MissingApplicationID(AppCommandError): + """An exception raised when the client does not have an application ID set. + An application ID is required for syncing application commands. + + This inherits from :exc:`~discord.app_commands.AppCommandError`. + + .. versionadded:: 2.0 + """ + + def __init__(self, message: Optional[str] = None): + super().__init__(message or APP_ID_NOT_FOUND) diff --git a/discord/app_commands/models.py b/discord/app_commands/models.py index 0a5889780..339814877 100644 --- a/discord/app_commands/models.py +++ b/discord/app_commands/models.py @@ -25,11 +25,11 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations from datetime import datetime - +from .errors import MissingApplicationID from ..permissions import Permissions from ..enums import AppCommandOptionType, AppCommandType, ChannelType, try_enum from ..mixins import Hashable -from ..utils import _get_as_snowflake, parse_time, snowflake_time +from ..utils import _get_as_snowflake, parse_time, snowflake_time, MISSING from typing import Generic, List, TYPE_CHECKING, Optional, TypeVar, Union __all__ = ( @@ -138,6 +138,9 @@ class AppCommand(Hashable): The default member permissions that can run this command. dm_permission: :class:`bool` A boolean that indicates whether this command can be run in direct messages. + 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__ = ( @@ -146,14 +149,15 @@ class AppCommand(Hashable): 'application_id', 'name', 'description', + 'guild_id', 'options', 'default_member_permissions', 'dm_permission', '_state', ) - def __init__(self, *, data: ApplicationCommandPayload, state: Optional[ConnectionState] = None) -> None: - self._state: Optional[ConnectionState] = state + def __init__(self, *, data: ApplicationCommandPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state self._from_data(data) def _from_data(self, data: ApplicationCommandPayload) -> None: @@ -161,6 +165,7 @@ class AppCommand(Hashable): self.application_id: int = int(data['application_id']) self.name: str = data['name'] self.description: str = data['description'] + self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') self.type: AppCommandType = try_enum(AppCommandType, data.get('type', 1)) self.options: List[Union[Argument, AppCommandGroup]] = [ app_command_option_factory(data=d, parent=self, state=self._state) for d in data.get('options', []) @@ -195,6 +200,130 @@ class AppCommand(Hashable): def __repr__(self) -> str: return f'<{self.__class__.__name__} id={self.id!r} name={self.name!r} type={self.type!r}>' + @property + def guild(self) -> Optional[Guild]: + """Optional[:class:`~discord.Guild`]: Returns the guild this command is registered to + if it exists. + """ + return self._state._get_guild(self.guild_id) + + async def delete(self) -> None: + """|coro| + + Deletes the application command. + + Raises + ------- + NotFound + The application command was not found. + Forbidden + You do not have permission to delete this application command. + HTTPException + Deleting the application command failed. + MissingApplicationID + The client does not have an application ID. + """ + state = self._state + if not state.application_id: + raise MissingApplicationID + + if self.guild_id: + await state.http.delete_guild_command( + state.application_id, + self.guild_id, + self.id, + ) + else: + await state.http.delete_global_command( + state.application_id, + self.id, + ) + + async def edit( + self, + *, + name: str = MISSING, + description: str = MISSING, + default_member_permissions: Optional[Permissions] = MISSING, + dm_permission: bool = MISSING, + options: List[Union[Argument, AppCommandGroup]] = MISSING, + ) -> AppCommand: + """|coro| + + Edits the application command. + + Parameters + ----------- + name: :class:`str` + The new name for the application command. + description: :class:`str` + The new description for the application command. + default_member_permissions: Optional[:class:`~discord.Permissions`] + The new default permissions needed to use this application command. + Pass value of ``None`` to remove any permission requirements. + dm_permission: :class:`bool` + Indicates if the application command can be used in DMs. + options: List[Union[:class:`Argument`, :class:`AppCommandGroup`]] + List of new options for this application command. + + Raises + ------- + NotFound + The application command was not found. + Forbidden + You do not have permission to edit this application command. + HTTPException + Editing the application command failed. + MissingApplicationID + The client does not have an application ID. + + Returns + -------- + :class:`AppCommand` + The newly edited application command. + """ + state = self._state + if not state.application_id: + raise MissingApplicationID + + payload = {} + + if name is not MISSING: + payload['name'] = name + + if description is not MISSING: + payload['description'] = description + + if default_member_permissions is not MISSING: + if default_member_permissions is not None: + payload['default_member_permissions'] = default_member_permissions.value + else: + payload['default_member_permissions'] = None + + if self.guild_id is None and dm_permission is not MISSING: + payload['dm_permission'] = dm_permission + + if options is not MISSING: + payload['options'] = [option.to_dict() for option in options] + + if not payload: + return self + + if self.guild_id: + data = await state.http.edit_guild_command( + state.application_id, + self.guild_id, + self.id, + payload, + ) + else: + data = await state.http.edit_global_command( + state.application_id, + self.id, + payload, + ) + return AppCommand(data=data, state=state) + class Choice(Generic[ChoiceT]): """Represents an application command argument choice. diff --git a/discord/app_commands/tree.py b/discord/app_commands/tree.py index a845e20f0..74eb6cb79 100644 --- a/discord/app_commands/tree.py +++ b/discord/app_commands/tree.py @@ -57,6 +57,7 @@ from .errors import ( CommandNotFound, CommandSignatureMismatch, CommandLimitReached, + MissingApplicationID, ) from ..errors import ClientException from ..enums import AppCommandType, InteractionType @@ -78,11 +79,6 @@ __all__ = ('CommandTree',) ClientT = TypeVar('ClientT', bound='Client') -APP_ID_NOT_FOUND = ( - 'Client does not have an application_id set. Either the function was called before on_ready ' - 'was called or application_id was not passed to the Client constructor.' -) - def _retrieve_guild_ids( command: Any, guild: Optional[Snowflake] = MISSING, guilds: Sequence[Snowflake] = MISSING @@ -158,7 +154,7 @@ class CommandTree(Generic[ClientT]): ------- HTTPException Fetching the commands failed. - ClientException + MissingApplicationID The application ID could not be found. Returns @@ -167,7 +163,7 @@ class CommandTree(Generic[ClientT]): The application's commands. """ if self.client.application_id is None: - raise ClientException(APP_ID_NOT_FOUND) + raise MissingApplicationID if guild is None: commands = await self._http.get_global_commands(self.client.application_id) @@ -906,7 +902,7 @@ class CommandTree(Generic[ClientT]): Syncing the commands failed. Forbidden The client does not have the ``applications.commands`` scope in the guild. - ClientException + MissingApplicationID The client does not have an application ID. Returns @@ -916,7 +912,7 @@ class CommandTree(Generic[ClientT]): """ if self.client.application_id is None: - raise ClientException(APP_ID_NOT_FOUND) + raise MissingApplicationID commands = self._get_all_commands(guild=guild) payload = [command.to_dict() for command in commands] diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 41200cbf8..49c167074 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -609,6 +609,9 @@ Exceptions .. autoexception:: discord.app_commands.CommandNotFound :members: +.. autoexception:: discord.app_commands.MissingApplicationID + :members: + Exception Hierarchy ~~~~~~~~~~~~~~~~~~~~ @@ -629,3 +632,4 @@ Exception Hierarchy - :exc:`~discord.app_commands.CommandAlreadyRegistered` - :exc:`~discord.app_commands.CommandSignatureMismatch` - :exc:`~discord.app_commands.CommandNotFound` + - :exc:`~discord.app_commands.MissingApplicationID`