From ed76909326236db8de9af5562ef671ca754eeeef Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 27 Jul 2022 01:25:17 -0400 Subject: [PATCH 01/18] feat: add ID attributes to ApplicationCommand classes --- disnake/app_commands.py | 17 +++++++++++++++++ disnake/client.py | 15 ++++++++++++++- disnake/ext/commands/interaction_bot_base.py | 12 ++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index b5f6345b6c..cec3ff088d 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -468,12 +468,14 @@ def __init__( name: LocalizedRequired, dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, + id: int = None, ): self.type: ApplicationCommandType = enum_if_int(ApplicationCommandType, type) name_loc = Localized._cast(name, True) self.name: str = name_loc.string self.name_localizations: LocalizationValue = name_loc.localizations + self.id: Optional[int] = id self.dm_permission: bool = True if dm_permission is None else dm_permission @@ -595,12 +597,14 @@ def __init__( name: LocalizedRequired, dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, + id: int = None, ): super().__init__( type=ApplicationCommandType.user, name=name, dm_permission=dm_permission, default_member_permissions=default_member_permissions, + id=id, ) @@ -636,6 +640,9 @@ class APIUserCommand(UserCommand, _APIApplicationCommandMixin): __repr_info__ = UserCommand.__repr_info__ + _APIApplicationCommandMixin.__repr_info__ + if TYPE_CHECKING: + id: int + @classmethod def from_dict(cls, data: ApplicationCommandPayload) -> Self: cmd_type = data.get("type", 0) @@ -678,12 +685,14 @@ def __init__( name: LocalizedRequired, dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, + id: int = None, ): super().__init__( type=ApplicationCommandType.message, name=name, dm_permission=dm_permission, default_member_permissions=default_member_permissions, + id=id, ) @@ -719,6 +728,9 @@ class APIMessageCommand(MessageCommand, _APIApplicationCommandMixin): __repr_info__ = MessageCommand.__repr_info__ + _APIApplicationCommandMixin.__repr_info__ + if TYPE_CHECKING: + id: int + @classmethod def from_dict(cls, data: ApplicationCommandPayload) -> Self: cmd_type = data.get("type", 0) @@ -779,12 +791,14 @@ def __init__( options: Optional[List[Option]] = None, dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, + id: int = None, ): super().__init__( type=ApplicationCommandType.chat_input, name=name, dm_permission=dm_permission, default_member_permissions=default_member_permissions, + id=id, ) _validate_name(self.name) @@ -898,6 +912,9 @@ class APISlashCommand(SlashCommand, _APIApplicationCommandMixin): __repr_info__ = SlashCommand.__repr_info__ + _APIApplicationCommandMixin.__repr_info__ + if TYPE_CHECKING: + id: int + @classmethod def from_dict(cls, data: ApplicationCommandPayload) -> Self: cmd_type = data.get("type", 0) diff --git a/disnake/client.py b/disnake/client.py index 296787939d..1cd31b7329 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -2478,6 +2478,10 @@ async def bulk_overwrite_global_commands( .. versionadded:: 2.1 + .. versionchanged:: 2.6 + + Modifies the commands' ``id`` attribute to correspond to the version on the API. + Parameters ---------- application_commands: List[:class:`.ApplicationCommand`] @@ -2490,7 +2494,16 @@ async def bulk_overwrite_global_commands( """ for cmd in application_commands: cmd.localize(self.i18n) - return await self._connection.bulk_overwrite_global_commands(application_commands) + res = await self._connection.bulk_overwrite_global_commands(application_commands) + + for api_command in res: + cmd = utils.get(application_commands, name=api_command.name) + if not cmd: + # consider a warning + continue + cmd.id = api_command.id + + return res # Application commands (guild) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 2577260cc1..32222f7c04 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -684,6 +684,14 @@ async def _cache_application_commands(self) -> None: } except (disnake.HTTPException, TypeError): pass + else: + for api_command in commands: + cmd = disnake.utils.get(self.all_slash_commands.values(), name=api_command.name) + if not cmd: + # consider logging + continue + cmd.body.id = api_command.id + for guild_id in guilds: try: commands = await self.fetch_guild_commands(guild_id, with_localizations=True) @@ -693,6 +701,9 @@ async def _cache_application_commands(self) -> None: } except (disnake.HTTPException, TypeError): pass + else: + # add ID info to guild slash commands as well. + ... async def _sync_application_commands(self) -> None: if not isinstance(self, disnake.Client): @@ -721,6 +732,7 @@ async def _sync_application_commands(self) -> None: f"| Update is required: {update_required}\n{_format_diff(diff)}" ) + # update all commands with their corresponding command if update_required: # Notice that we don't do any API requests if there're no changes. try: From 8e6cc3c19aa7cef6f03bc6fec50ee5e28730aacc Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sat, 1 Oct 2022 00:12:27 -0400 Subject: [PATCH 02/18] feat: make InvokableCommands also subclasses of ApplicationCommand classes --- disnake/ext/commands/base_core.py | 5 +- disnake/ext/commands/ctx_menus_core.py | 36 ++++++++++++-- disnake/ext/commands/slash_core.py | 68 +++++++++++++++++++++----- 3 files changed, 92 insertions(+), 17 deletions(-) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 48aa54eb4d..0982dfd2d1 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -134,7 +134,6 @@ def __init__(self, func: CommandCallback, *, name: Optional[str] = None, **kwarg self.__command_flag__ = None self._callback: CommandCallback = func self.name: str = name or func.__name__ - self.qualified_name: str = self.name # Annotation parser needs this attribute because body doesn't exist at this moment. # We will use this attribute later in order to set the dm_permission. self._guild_only: bool = kwargs.get("guild_only", False) @@ -241,6 +240,10 @@ def dm_permission(self) -> bool: """:class:`bool`: Whether this command can be used in DMs.""" return self.body.dm_permission + @property + def qualified_name(self) -> str: + return self.name + @property def default_member_permissions(self) -> Optional[Permissions]: """Optional[:class:`.Permissions`]: The default required member permissions for this command. diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 6f2c965879..41d0db17a6 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -3,9 +3,10 @@ from __future__ import annotations import asyncio -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Literal, Optional, Sequence, Tuple, Union from disnake.app_commands import MessageCommand, UserCommand +from disnake.enums import ApplicationCommandType from disnake.i18n import Localized from disnake.permissions import Permissions @@ -16,7 +17,7 @@ if TYPE_CHECKING: from typing_extensions import ParamSpec - from disnake.i18n import LocalizedOptional + from disnake.i18n import LocalizationValue, LocalizedOptional from disnake.interactions import ( ApplicationCommandInteraction, MessageCommandInteraction, @@ -30,7 +31,7 @@ __all__ = ("InvokableUserCommand", "InvokableMessageCommand", "user_command", "message_command") -class InvokableUserCommand(InvokableApplicationCommand): +class InvokableUserCommand(InvokableApplicationCommand, UserCommand): """A class that implements the protocol for a bot user command (context menu). These are not created manually, instead they are created via the @@ -103,6 +104,20 @@ def __init__( default_member_permissions=default_member_permissions, ) + self._name_localised = name_loc + + @property + def type(self) -> Literal[ApplicationCommandType.user]: + return ApplicationCommandType.user + + @property + def qualified_name(self) -> str: + return self.name + + @property + def name_localizations(self) -> LocalizationValue: + return self._name_localised.localizations + async def _call_external_error_handlers( self, inter: ApplicationCommandInteraction, error: CommandError ) -> None: @@ -130,7 +145,7 @@ async def __call__( await safe_call(self.callback, interaction, *args, **kwargs) -class InvokableMessageCommand(InvokableApplicationCommand): +class InvokableMessageCommand(InvokableApplicationCommand, MessageCommand): """A class that implements the protocol for a bot message command (context menu). These are not created manually, instead they are created via the @@ -196,6 +211,19 @@ def __init__( dm_permission=dm_permission and not self._guild_only, default_member_permissions=default_member_permissions, ) + self._name_localised = name_loc + + @property + def type(self) -> Literal[ApplicationCommandType.message]: + return ApplicationCommandType.message + + @property + def qualified_name(self) -> str: + return self.name + + @property + def name_localizations(self) -> LocalizationValue: + return self._name_localised.localizations async def _call_external_error_handlers( self, inter: ApplicationCommandInteraction, error: CommandError diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index 57c855bb7d..0b2c0517bb 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -10,6 +10,7 @@ Callable, Dict, List, + Literal, Optional, Sequence, Tuple, @@ -19,7 +20,7 @@ from disnake import utils from disnake.app_commands import Option, SlashCommand -from disnake.enums import OptionType +from disnake.enums import ApplicationCommandType, OptionType from disnake.i18n import Localized from disnake.interactions import ApplicationCommandInteraction from disnake.permissions import Permissions @@ -30,7 +31,7 @@ if TYPE_CHECKING: from disnake.app_commands import Choices - from disnake.i18n import LocalizedOptional + from disnake.i18n import LocalizationValue, LocalizedOptional from .base_core import CommandCallback @@ -146,7 +147,7 @@ def __init__( ): name_loc = Localized._cast(name, False) super().__init__(func, name=name_loc.string, **kwargs) - self.parent: InvokableSlashCommand = parent + self._parent: InvokableSlashCommand = parent self.children: Dict[str, SubCommand] = {} self.option = Option( name=name_loc._upgrade(self.name), @@ -154,7 +155,6 @@ def __init__( type=OptionType.sub_command_group, options=[], ) - self.qualified_name: str = f"{parent.qualified_name} {self.name}" if ( "dm_permission" in kwargs @@ -172,7 +172,7 @@ def root_parent(self) -> InvokableSlashCommand: .. versionadded:: 2.6 """ - return self.parent + return self._parent @property def parents(self) -> Tuple[InvokableSlashCommand]: @@ -180,12 +180,20 @@ def parents(self) -> Tuple[InvokableSlashCommand]: .. versionadded:: 2.6 """ - return (self.parent,) + return (self._parent,) @property def body(self) -> Option: return self.option + @property + def qualified_name(self) -> str: + return f"{self._parent.qualified_name} {self.name}" + + @property + def parent(self) -> Optional[InvokableSlashCommand]: + return self._parent + def sub_command( self, name: LocalizedOptional = None, @@ -216,6 +224,7 @@ def decorator(func: CommandCallback) -> SubCommand: extras=extras, **kwargs, ) + new_func._parent = self self.children[new_func.name] = new_func self.option.options.append(new_func.option) return new_func @@ -262,6 +271,9 @@ class SubCommand(InvokableApplicationCommand): This object may be copied by the library. .. versionadded:: 2.5 + parent: ... + + .. versionadded:: 2.6 """ def __init__( @@ -277,7 +289,7 @@ def __init__( ): name_loc = Localized._cast(name, False) super().__init__(func, name=name_loc.string, **kwargs) - self.parent: Union[InvokableSlashCommand, SubCommandGroup] = parent + self._parent: Union[InvokableSlashCommand, SubCommandGroup] = parent self.connectors: Dict[str, str] = connectors or {} self.autocompleters: Dict[str, Any] = kwargs.get("autocompleters", {}) @@ -295,7 +307,6 @@ def __init__( type=OptionType.sub_command, options=options, ) - self.qualified_name = f"{parent.qualified_name} {self.name}" if ( "dm_permission" in kwargs @@ -339,6 +350,16 @@ def description(self) -> str: def body(self) -> Option: return self.option + @property + def qualified_name(self) -> str: + if not self._parent: + return self.name + return f"{self._parent.qualified_name} {self.name}" + + @property + def parent(self) -> Optional[Union[InvokableSlashCommand, SubCommandGroup]]: + return self._parent + async def _call_autocompleter( self, param: str, inter: ApplicationCommandInteraction, user_input: str ) -> Optional[Choices]: @@ -379,7 +400,7 @@ def autocomplete(self, option_name: str) -> Callable[[Callable], Callable]: return _autocomplete(self, option_name) -class InvokableSlashCommand(InvokableApplicationCommand): +class InvokableSlashCommand(InvokableApplicationCommand, SlashCommand): """A class that implements the protocol for a bot slash command. These are not created manually, instead they are created via the @@ -440,8 +461,11 @@ def __init__( **kwargs, ): name_loc = Localized._cast(name, False) - super().__init__(func, name=name_loc.string, **kwargs) - self.parent = None + super().__init__( + func, + name=name_loc.string, + **kwargs, + ) self.connectors: Dict[str, str] = connectors or {} self.children: Dict[str, Union[SubCommand, SubCommandGroup]] = {} self.auto_sync: bool = True if auto_sync is None else auto_sync @@ -453,6 +477,8 @@ def __init__( self.docstring = utils.parse_docstring(func) desc_loc = Localized._cast(description, False) + self._name_localised = name_loc + self._description_localised = desc_loc try: default_member_permissions = func.__default_member_permissions__ @@ -497,7 +523,7 @@ def _ensure_assignment_on_copy(self, other: SlashCommandT) -> SlashCommandT: other.children = self.children.copy() # update parents... for child in other.children.values(): - child.parent = other + child._parent = other if self.description != other.description and "description" not in other.__original_kwargs__: # Allows overriding the default description cog-wide. other.body.description = self.description @@ -505,6 +531,10 @@ def _ensure_assignment_on_copy(self, other: SlashCommandT) -> SlashCommandT: other.body.options = self.options return other + @property + def type(self) -> Literal[ApplicationCommandType.chat_input]: + return ApplicationCommandType.chat_input + @property def description(self) -> str: return self.body.description @@ -513,6 +543,18 @@ def description(self) -> str: def options(self) -> List[Option]: return self.body.options + @property + def name_localizations(self) -> LocalizationValue: + return self._name_localised.localizations + + @property + def description_localizations(self) -> LocalizationValue: + return self._description_localised.localizations + + add_option = SlashCommand.add_option + to_dict = SlashCommand.to_dict + localize = SlashCommand.localize + def sub_command( self, name: LocalizedOptional = None, @@ -573,6 +615,7 @@ def decorator(func: CommandCallback) -> SubCommand: extras=extras, **kwargs, ) + new_func._parent = self self.children[new_func.name] = new_func self.body.options.append(new_func.option) return new_func @@ -619,6 +662,7 @@ def decorator(func: CommandCallback) -> SubCommandGroup: extras=extras, **kwargs, ) + new_func._parent = self self.children[new_func.name] = new_func self.body.options.append(new_func.option) return new_func From a62168a0fb4f9ef5144742aacf58def6df7e7ed8 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 27 Jul 2022 03:26:42 -0400 Subject: [PATCH 03/18] feat: support providing ids to sync commands --- disnake/app_commands.py | 8 ++++++ disnake/client.py | 3 ++- disnake/ext/commands/base_core.py | 4 +++ disnake/ext/commands/ctx_menus_core.py | 4 +++ disnake/ext/commands/interaction_bot_base.py | 26 +++++++++++++------- disnake/ext/commands/slash_core.py | 2 ++ disnake/types/interactions.py | 2 ++ 7 files changed, 39 insertions(+), 10 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index cec3ff088d..b92c7a0b74 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -537,6 +537,11 @@ def __eq__(self, other) -> bool: and self._default_permission == other._default_permission ) + def need_sync(self, other: ApplicationCommand) -> bool: + if self != other: + return True + return self.id is not None and self.id != other.id + def to_dict(self) -> EditApplicationCommandPayload: data: EditApplicationCommandPayload = { "type": try_enum_to_int(self.type), @@ -552,6 +557,9 @@ def to_dict(self) -> EditApplicationCommandPayload: if (loc := self.name_localizations.data) is not None: data["name_localizations"] = loc + if self.id is not None: + data["id"] = self.id + return data def localize(self, store: LocalizationProtocol) -> None: diff --git a/disnake/client.py b/disnake/client.py index 1cd31b7329..933406abb1 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -2501,7 +2501,8 @@ async def bulk_overwrite_global_commands( if not cmd: # consider a warning continue - cmd.id = api_command.id + if cmd.id is None: + cmd.id = api_command.id return res diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 0982dfd2d1..365df246c6 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -235,6 +235,10 @@ def _update_copy(self: AppCommandT, kwargs: Dict[str, Any]) -> AppCommandT: else: return self.copy() + @property + def id(self) -> Optional[int]: + return self.body.id + @property def dm_permission(self) -> bool: """:class:`bool`: Whether this command can be used in DMs.""" diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 41d0db17a6..470e02cc18 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -78,6 +78,7 @@ def __init__( default_member_permissions: Optional[Union[Permissions, int]] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, + id: Optional[int] = None, **kwargs, ): name_loc = Localized._cast(name, False) @@ -102,6 +103,7 @@ def __init__( name=name_loc._upgrade(self.name), dm_permission=dm_permission and not self._guild_only, default_member_permissions=default_member_permissions, + id=id, ) self._name_localised = name_loc @@ -192,6 +194,7 @@ def __init__( default_member_permissions: Optional[Union[Permissions, int]] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, + id: Optional[int] = None, **kwargs, ): name_loc = Localized._cast(name, False) @@ -210,6 +213,7 @@ def __init__( name=name_loc._upgrade(self.name), dm_permission=dm_permission and not self._guild_only, default_member_permissions=default_member_permissions, + id=id, ) self._name_localised = name_loc diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 32222f7c04..f71d1fae9b 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -89,7 +89,7 @@ def _app_commands_diff( elif new_cmd._always_synced: diff["no_changes"].append(old_cmd) continue - elif new_cmd != old_cmd: + elif new_cmd.need_sync(old_cmd): diff["edit"].append(new_cmd) else: diff["no_changes"].append(new_cmd) @@ -684,13 +684,6 @@ async def _cache_application_commands(self) -> None: } except (disnake.HTTPException, TypeError): pass - else: - for api_command in commands: - cmd = disnake.utils.get(self.all_slash_commands.values(), name=api_command.name) - if not cmd: - # consider logging - continue - cmd.body.id = api_command.id for guild_id in guilds: try: @@ -732,7 +725,6 @@ async def _sync_application_commands(self) -> None: f"| Update is required: {update_required}\n{_format_diff(diff)}" ) - # update all commands with their corresponding command if update_required: # Notice that we don't do any API requests if there're no changes. try: @@ -788,8 +780,24 @@ async def _prepare_application_commands(self) -> None: await self.wait_until_first_connect() await self._cache_application_commands() await self._sync_application_commands() + self._fill_app_command_ids() self._sync_queued = False + def _fill_app_command_ids(self): + # this requires that _cache_application_commands was ran first + if not isinstance(self, disnake.Client): + raise NotImplementedError("This method is only usable in disnake.Client subclasses") + + all_commands = self.all_slash_commands.values() + for api_command in self._connection._global_application_commands.values(): + cmd = disnake.utils.get(all_commands, name=api_command.name) + if not cmd: + # consider logging + continue + # todo: think about body usage + if cmd.body.id is None: + cmd.body.id = api_command.id + async def _delayed_command_sync(self) -> None: if not isinstance(self, disnake.Client): raise NotImplementedError("This method is only usable in disnake.Client subclasses") diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index 0b2c0517bb..b443a460a5 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -458,6 +458,7 @@ def __init__( guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, auto_sync: Optional[bool] = None, + id: Optional[int] = None, **kwargs, ): name_loc = Localized._cast(name, False) @@ -495,6 +496,7 @@ def __init__( options=options or [], dm_permission=dm_permission and not self._guild_only, default_member_permissions=default_member_permissions, + id=id, ) @property diff --git a/disnake/types/interactions.py b/disnake/types/interactions.py index faac07ce9a..d4d5de2683 100644 --- a/disnake/types/interactions.py +++ b/disnake/types/interactions.py @@ -305,6 +305,8 @@ class InteractionMessageReference(TypedDict): class EditApplicationCommand(TypedDict): name: str + # TODO: properly seperate these payloads + id: NotRequired[Snowflake] # when this is provided we are able to change the name, this is also slightly wrong in this payload name_localizations: NotRequired[Optional[ApplicationCommandLocalizations]] description: NotRequired[str] description_localizations: NotRequired[Optional[ApplicationCommandLocalizations]] From 8595828682e4805360a2927a66b134131da3f3a1 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 27 Jul 2022 03:43:07 -0400 Subject: [PATCH 04/18] fix: if a user-provided command ID is invalid ignore it --- disnake/client.py | 3 +-- disnake/ext/commands/base_core.py | 4 ++++ disnake/ext/commands/interaction_bot_base.py | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/disnake/client.py b/disnake/client.py index 933406abb1..1cd31b7329 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -2501,8 +2501,7 @@ async def bulk_overwrite_global_commands( if not cmd: # consider a warning continue - if cmd.id is None: - cmd.id = api_command.id + cmd.id = api_command.id return res diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 365df246c6..8d66387cb2 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -239,6 +239,10 @@ def _update_copy(self: AppCommandT, kwargs: Dict[str, Any]) -> AppCommandT: def id(self) -> Optional[int]: return self.body.id + @id.setter + def id(self, id: int): + self.body.id = id + @property def dm_permission(self) -> bool: """:class:`bool`: Whether this command can be used in DMs.""" diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index f71d1fae9b..b93d8a7174 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -679,11 +679,15 @@ async def _cache_application_commands(self) -> None: try: commands = await self.fetch_global_commands(with_localizations=True) - self._connection._global_application_commands = { + self._connection._global_application_commands = global_commands = { command.id: command for command in commands } except (disnake.HTTPException, TypeError): pass + else: + for command in self.application_commands_iterator(): + if command.body.id is not None and command.body.id not in global_commands: + command.body.id = None for guild_id in guilds: try: From 237ac58f952ab9542403bedd130d3939ea7769a6 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 27 Jul 2022 05:09:13 -0400 Subject: [PATCH 05/18] feat: move command sync config to flags --- disnake/ext/commands/__init__.py | 1 + disnake/ext/commands/bot.py | 5 +++++ disnake/ext/commands/cog.py | 4 ++-- disnake/ext/commands/interaction_bot_base.py | 20 ++++++++++---------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/disnake/ext/commands/__init__.py b/disnake/ext/commands/__init__.py index c97b3a0ebf..6a59dde1c9 100644 --- a/disnake/ext/commands/__init__.py +++ b/disnake/ext/commands/__init__.py @@ -21,6 +21,7 @@ from .custom_warnings import * from .errors import * from .flag_converter import * +from .flags import * from .help import * from .params import * from .slash_core import * diff --git a/disnake/ext/commands/bot.py b/disnake/ext/commands/bot.py index 3b0d82cb25..f685ac00df 100644 --- a/disnake/ext/commands/bot.py +++ b/disnake/ext/commands/bot.py @@ -61,6 +61,11 @@ class Bot(BotBase, InteractionBotBase, disnake.Client): .. versionadded:: 2.1 + command_sync: :class:`ApplicationCommandSyncFlags` + The configuration for application command sync. + + .. versionadded:: 2.6 + sync_commands: :class:`bool` Whether to enable automatic synchronization of application commands in your code. Defaults to ``True``, which means that commands in API are automatically synced diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index 54b33e9c55..b493edf685 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -808,7 +808,7 @@ def _inject(self, bot: AnyBot) -> Self: bot.add_listener(getattr(self, method_name), name) try: - if bot._sync_commands_on_cog_unload: + if bot._command_sync.on_cog_unload: bot._schedule_delayed_command_sync() except NotImplementedError: pass @@ -874,7 +874,7 @@ def _eject(self, bot: AnyBot) -> None: finally: try: - if bot._sync_commands_on_cog_unload: + if bot._command_sync.on_cog_unload: bot._schedule_delayed_command_sync() except NotImplementedError: pass diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index b93d8a7174..c74e4cc20d 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -38,6 +38,7 @@ user_command, ) from .errors import CommandRegistrationError +from .flags import ApplicationCommandSyncFlags from .slash_core import InvokableSlashCommand, SubCommand, SubCommandGroup, slash_command if TYPE_CHECKING: @@ -125,9 +126,7 @@ class InteractionBotBase(CommonBotBase): def __init__( self, *, - sync_commands: bool = True, - sync_commands_debug: bool = False, - sync_commands_on_cog_unload: bool = True, + command_sync: ApplicationCommandSyncFlags = None, test_guilds: Optional[Sequence[int]] = None, **options: Any, ): @@ -138,10 +137,11 @@ def __init__( test_guilds = None if test_guilds is None else tuple(test_guilds) self._test_guilds: Optional[Tuple[int, ...]] = test_guilds - self._sync_commands: bool = sync_commands - self._sync_commands_debug: bool = sync_commands_debug - self._sync_commands_on_cog_unload = sync_commands_on_cog_unload + if command_sync is None: + command_sync = ApplicationCommandSyncFlags.default() + self._sync_queued: bool = False + self._command_sync = command_sync self._slash_command_checks = [] self._slash_command_check_once = [] @@ -706,7 +706,7 @@ async def _sync_application_commands(self) -> None: if not isinstance(self, disnake.Client): raise NotImplementedError("This method is only usable in disnake.Client subclasses") - if not self._sync_commands or self._is_closed or self.loop.is_closed(): + if not self._command_sync.sync_commands or self._is_closed or self.loop.is_closed(): return # We assume that all commands are already cached. @@ -764,7 +764,7 @@ async def _sync_application_commands(self) -> None: self._log_sync_debug("Command synchronization task has finished") def _log_sync_debug(self, text: str) -> None: - if self._sync_commands_debug: + if self._command_sync.sync_commands_debug: # if sync debugging is enabled, *always* output logs if _log.isEnabledFor(logging.INFO): # if the log level is `INFO` or higher, use that @@ -807,7 +807,7 @@ async def _delayed_command_sync(self) -> None: raise NotImplementedError("This method is only usable in disnake.Client subclasses") if ( - not self._sync_commands + not self._command_sync.sync_commands or self._sync_queued or not self.is_ready() or self._is_closed @@ -1225,7 +1225,7 @@ async def process_application_commands( interaction: :class:`disnake.ApplicationCommandInteraction` The interaction to process commands for. """ - if self._sync_commands and not self._sync_queued: + if self._command_sync.sync_commands and not self._sync_queued: known_command = self.get_global_command(interaction.data.id) # type: ignore if known_command is None: From a625e20c638369673d85f794fc96b095d31cae7e Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 27 Jul 2022 05:46:32 -0400 Subject: [PATCH 06/18] feat: add never_delete and global or guild command syncing options --- disnake/ext/commands/flags.py | 174 +++++++++++++++++++ disnake/ext/commands/interaction_bot_base.py | 100 ++++++----- 2 files changed, 233 insertions(+), 41 deletions(-) create mode 100644 disnake/ext/commands/flags.py diff --git a/disnake/ext/commands/flags.py b/disnake/ext/commands/flags.py new file mode 100644 index 0000000000..a31354bb07 --- /dev/null +++ b/disnake/ext/commands/flags.py @@ -0,0 +1,174 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Disnake Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from disnake.flags import BaseFlags, alias_flag_value, all_flags_value, fill_with_flags, flag_value + +if TYPE_CHECKING: + from typing_extensions import Self + +__all__ = ("ApplicationCommandSyncFlags",) + + +@fill_with_flags() +class ApplicationCommandSyncFlags(BaseFlags): + """Controls the library's application command syncing policy. + + This allows for finer grained control over what commands are synced automatically and in what cases. + + To construct an object you can pass keyword arguments denoting the flags + to enable or disable. + + The default value is all flags enabled. + + .. versionadded:: 2.6 + + .. container:: operations + + .. describe:: x == y + + Checks if two ApplicationCommandSyncFlags instances are equal. + .. describe:: x != y + + Checks if two ApplicationCommandSyncFlags instances are not equal. + .. describe:: x <= y + + Checks if an ApplicationCommandSyncFlags instance is a subset of another ApplicationCommandSyncFlags instance. + .. describe:: x >= y + + Checks if an ApplicationCommandSyncFlags instance is a superset of another ApplicationCommandSyncFlags instance. + .. describe:: x < y + + Checks if an ApplicationCommandSyncFlags instance is a strict subset of another ApplicationCommandSyncFlags instance. + .. describe:: x > y + + Checks if an ApplicationCommandSyncFlags instance is a strict superset of another ApplicationCommandSyncFlags instance. + .. describe:: x | y, x |= y + + Returns a new ApplicationCommandSyncFlags instance with all enabled flags from both x and y. + (Using ``|=`` will update in place). + .. describe:: x & y, x &= y + + Returns a new ApplicationCommandSyncFlags instance with only flags enabled on both x and y. + (Using ``&=`` will update in place). + .. describe:: x ^ y, x ^= y + + Returns a new ApplicationCommandSyncFlags instance with only flags enabled on one of x or y, but not both. + (Using ``^=`` will update in place). + .. describe:: ~x + + Returns a new ApplicationCommandSyncFlags instance with all flags from x inverted. + .. describe:: hash(x) + + Return the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + + Additionally supported are a few operations on class attributes. + + .. describe:: ApplicationCommandSyncFlags.y | ApplicationCommandSyncFlags.z, ApplicationCommandSyncFlags(y=True) | ApplicationCommandSyncFlags.z + + Returns a ApplicationCommandSyncFlags instance with all provided flags enabled. + + .. describe:: ~ApplicationCommandSyncFlags.y + + Returns a ApplicationCommandSyncFlags instance with all flags except ``y`` inverted from their default value. + + .. versionadded:: 2.6 + Attributes + ---------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + __slots__ = () + + def __init__(self, **kwargs: bool): + self.value = all_flags_value(self.VALID_FLAGS) + for key, value in kwargs.items(): + if key not in self.VALID_FLAGS: + raise TypeError(f"{key!r} is not a valid flag name.") + setattr(self, key, value) + + @classmethod + def all(cls) -> Self: + """A factory method that creates a :class:`ApplicationCommandSyncFlags` with everything enabled.""" + self = cls.__new__(cls) + self.value = all_flags_value(cls.VALID_FLAGS) + return self + + @classmethod + def none(cls) -> Self: + """A factory method that creates a :class:`ApplicationCommandSyncFlags` with everything disabled.""" + self = cls.__new__(cls) + self.value = self.DEFAULT_VALUE + return self + + @classmethod + def default(cls) -> Self: + """A factory method that creates a :class:`ApplicationCommandSyncFlags` with the default settings.""" + instance = cls.all() + instance.sync_commands_debug = False + instance.never_delete = False + return instance + + @alias_flag_value + def sync_commands(self): + """:class:`bool`: Whether to sync app commands at all.""" + return 1 << 5 | 1 << 6 + + @flag_value + def sync_commands_debug(self): + """:class:`bool`: Whether or not to show app command sync debug messages""" + return 1 << 1 + + @alias_flag_value + def on_cog_actions(self): + """:class:`bool`: Whether or not to sync app commands on cog load, unload, or reload.""" + return 1 << 2 | 1 << 4 + + @flag_value + def on_cog_unload(self): + """:class:`bool`: Whether or not to sync app commands on cog unload or reload.""" + return 1 << 2 + + @flag_value + def never_delete(self): + return 1 << 3 + + @flag_value + def global_commands(self): + return 1 << 5 + + @flag_value + def guild_commands(self): + return 1 << 6 diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index c74e4cc20d..25b91bc414 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -19,6 +19,7 @@ Sequence, Set, Tuple, + TypedDict, TypeVar, Union, ) @@ -69,14 +70,21 @@ _log = logging.getLogger(__name__) +class _Diff(TypedDict): + no_changes: List[ApplicationCommand] + upsert: List[ApplicationCommand] + edit: List[ApplicationCommand] + delete: List[ApplicationCommand] + + def _app_commands_diff( new_commands: Iterable[ApplicationCommand], old_commands: Iterable[ApplicationCommand], -) -> Dict[str, List[ApplicationCommand]]: +) -> _Diff: new_cmds = {(cmd.name, cmd.type): cmd for cmd in new_commands} old_cmds = {(cmd.name, cmd.type): cmd for cmd in old_commands} - diff = { + diff: _Diff = { "no_changes": [], "upsert": [], "edit": [], @@ -110,7 +118,7 @@ def _app_commands_diff( } -def _format_diff(diff: Dict[str, List[ApplicationCommand]]) -> str: +def _format_diff(diff: _Diff) -> str: lines: List[str] = [] for key, label in _diff_map.items(): lines.append(label) @@ -712,54 +720,64 @@ async def _sync_application_commands(self) -> None: # We assume that all commands are already cached. # Sort all invokable commands between guild IDs: global_cmds, guild_cmds = self._ordered_unsynced_commands(self._test_guilds) - if global_cmds is None: - return - # Update global commands first - diff = _app_commands_diff( - global_cmds, self._connection._global_application_commands.values() - ) - update_required = bool(diff["upsert"]) or bool(diff["edit"]) or bool(diff["delete"]) - - # Show the difference - self._log_sync_debug( - "Application command synchronization:\n" - "GLOBAL COMMANDS\n" - "===============\n" - f"| Update is required: {update_required}\n{_format_diff(diff)}" - ) - - if update_required: - # Notice that we don't do any API requests if there're no changes. - try: - to_send = diff["no_changes"] + diff["edit"] + diff["upsert"] - await self.bulk_overwrite_global_commands(to_send) - except Exception as e: - warnings.warn(f"Failed to overwrite global commands due to {e}", SyncWarning) - # Same process but for each specified guild individually. - # Notice that we're not doing this for every single guild for optimisation purposes. - # See the note in :meth:`_cache_application_commands` about guild app commands. - for guild_id, cmds in guild_cmds.items(): - current_guild_cmds = self._connection._guild_application_commands.get(guild_id, {}) - diff = _app_commands_diff(cmds, current_guild_cmds.values()) + if global_cmds is not None and self._command_sync.global_commands: + # Update global commands first + diff = _app_commands_diff( + global_cmds, self._connection._global_application_commands.values() + ) + if self._command_sync.never_delete: + # because never_delete is enabled, we want to never delete a command, so we move the delete commands to no_changes + diff["no_changes"] += diff["delete"] + diff["delete"].clear() update_required = bool(diff["upsert"]) or bool(diff["edit"]) or bool(diff["delete"]) - # Show diff + + # Show the difference self._log_sync_debug( "Application command synchronization:\n" - f"COMMANDS IN {guild_id}\n" - "===============================\n" + "GLOBAL COMMANDS\n" + "===============\n" f"| Update is required: {update_required}\n{_format_diff(diff)}" ) - # Do API requests and cache + if update_required: + # Notice that we don't do any API requests if there're no changes. try: to_send = diff["no_changes"] + diff["edit"] + diff["upsert"] - await self.bulk_overwrite_guild_commands(guild_id, to_send) + await self.bulk_overwrite_global_commands(to_send) except Exception as e: - warnings.warn( - f"Failed to overwrite commands in due to {e}", - SyncWarning, - ) + warnings.warn(f"Failed to overwrite global commands due to {e}", SyncWarning) + # Same process but for each specified guild individually. + # Notice that we're not doing this for every single guild for optimisation purposes. + # See the note in :meth:`_cache_application_commands` about guild app commands. + if guild_cmds is not None and self._command_sync.guild_commands: + for guild_id, cmds in guild_cmds.items(): + current_guild_cmds = self._connection._guild_application_commands.get(guild_id, {}) + diff = _app_commands_diff(cmds, current_guild_cmds.values()) + if self._command_sync.never_delete: + # because never_delete is enabled, we want to never delete a command, so we move the delete commands to no_changes + diff["no_changes"] += diff["delete"] + diff["delete"].clear() + update_required = bool(diff["upsert"]) or bool(diff["edit"]) or bool(diff["delete"]) + + # Show diff + self._log_sync_debug( + "Application command synchronization:\n" + f"COMMANDS IN {guild_id}\n" + "===============================\n" + f"| Update is required: {update_required}\n{_format_diff(diff)}" + ) + + # Do API requests and cache + if update_required: + try: + to_send = diff["no_changes"] + diff["edit"] + diff["upsert"] + await self.bulk_overwrite_guild_commands(guild_id, to_send) + except Exception as e: + warnings.warn( + f"Failed to overwrite commands in due to {e}", + SyncWarning, + ) # Last debug message self._log_sync_debug("Command synchronization task has finished") From a1356e817ad88758182da111607b1b53f41a580f Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 27 Jul 2022 06:09:08 -0400 Subject: [PATCH 07/18] chore: invert never_delete to allow deletion --- disnake/ext/commands/flags.py | 3 +-- disnake/ext/commands/interaction_bot_base.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/disnake/ext/commands/flags.py b/disnake/ext/commands/flags.py index a31354bb07..ae51d7bf32 100644 --- a/disnake/ext/commands/flags.py +++ b/disnake/ext/commands/flags.py @@ -138,7 +138,6 @@ def default(cls) -> Self: """A factory method that creates a :class:`ApplicationCommandSyncFlags` with the default settings.""" instance = cls.all() instance.sync_commands_debug = False - instance.never_delete = False return instance @alias_flag_value @@ -162,7 +161,7 @@ def on_cog_unload(self): return 1 << 2 @flag_value - def never_delete(self): + def allow_command_deletion(self): return 1 << 3 @flag_value diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 25b91bc414..2834e73f60 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -726,8 +726,8 @@ async def _sync_application_commands(self) -> None: diff = _app_commands_diff( global_cmds, self._connection._global_application_commands.values() ) - if self._command_sync.never_delete: - # because never_delete is enabled, we want to never delete a command, so we move the delete commands to no_changes + if not self._command_sync.allow_command_deletion: + # because allow_command_deletion is disabled, we want to never delete a command, so we move the delete commands to no_changes diff["no_changes"] += diff["delete"] diff["delete"].clear() update_required = bool(diff["upsert"]) or bool(diff["edit"]) or bool(diff["delete"]) @@ -754,8 +754,8 @@ async def _sync_application_commands(self) -> None: for guild_id, cmds in guild_cmds.items(): current_guild_cmds = self._connection._guild_application_commands.get(guild_id, {}) diff = _app_commands_diff(cmds, current_guild_cmds.values()) - if self._command_sync.never_delete: - # because never_delete is enabled, we want to never delete a command, so we move the delete commands to no_changes + if self._command_sync.allow_command_deletion: + # because allow_command_deletion is disabled, we want to never delete a command, so we move the delete commands to no_changes diff["no_changes"] += diff["delete"] diff["delete"].clear() update_required = bool(diff["upsert"]) or bool(diff["edit"]) or bool(diff["delete"]) From 8fca143a0d29414a391354d1382412692eb289a3 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 27 Jul 2022 12:45:31 -0400 Subject: [PATCH 08/18] feat: containerize each slash command type resolves disnake#260 --- disnake/ext/commands/errors.py | 7 +- disnake/ext/commands/interaction_bot_base.py | 242 +++++++++++++++---- 2 files changed, 204 insertions(+), 45 deletions(-) diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index 47cc5762f7..7bbdf53d55 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -1016,11 +1016,14 @@ class CommandRegistrationError(ClientException): Whether the name that conflicts is an alias of the command we try to add. """ - def __init__(self, name: str, *, alias_conflict: bool = False) -> None: + def __init__(self, name: str, *, alias_conflict: bool = False, guild_id: int = None) -> None: self.name: str = name self.alias_conflict: bool = alias_conflict + self.guild_id: Optional[int] = guild_id type_ = "alias" if alias_conflict else "command" - super().__init__(f"The {type_} {name} is already an existing command or alias.") + msg = f"The {type_} {name} is already an existing command or alias" + msg += "." if not guild_id else f" in guild ID {guild_id}." + super().__init__(msg) class FlagError(BadArgument): diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 2834e73f60..fc657cde00 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -15,6 +15,8 @@ Dict, Iterable, List, + Literal, + NamedTuple, Optional, Sequence, Set, @@ -22,6 +24,7 @@ TypedDict, TypeVar, Union, + overload, ) import disnake @@ -77,6 +80,12 @@ class _Diff(TypedDict): delete: List[ApplicationCommand] +class AppCommandMetadata(NamedTuple): + name: str + guild_id: Optional[int] + type: ApplicationCommandType + + def _app_commands_diff( new_commands: Iterable[ApplicationCommand], old_commands: Iterable[ApplicationCommand], @@ -165,19 +174,19 @@ def __init__( self._before_message_command_invoke = None self._after_message_command_invoke = None - self.all_slash_commands: Dict[str, InvokableSlashCommand] = {} - self.all_user_commands: Dict[str, InvokableUserCommand] = {} - self.all_message_commands: Dict[str, InvokableMessageCommand] = {} + self._all_app_commands: Dict[AppCommandMetadata, InvokableApplicationCommand] = {} self._schedule_app_command_preparation() def application_commands_iterator(self) -> Iterable[InvokableApplicationCommand]: return chain( - self.all_slash_commands.values(), - self.all_user_commands.values(), - self.all_message_commands.values(), + self._all_app_commands.values(), ) + @property + def all_app_commands(self) -> List[InvokableApplicationCommand]: + return list(self._all_app_commands.values()) + @property def application_commands(self) -> Set[InvokableApplicationCommand]: """Set[:class:`InvokableApplicationCommand`]: A set of all application commands the bot has.""" @@ -186,17 +195,32 @@ def application_commands(self) -> Set[InvokableApplicationCommand]: @property def slash_commands(self) -> Set[InvokableSlashCommand]: """Set[:class:`InvokableSlashCommand`]: A set of all slash commands the bot has.""" - return set(self.all_slash_commands.values()) + return { + command + for meta, command in self._all_app_commands + if meta.type is ApplicationCommandType.chat_input + } @property def user_commands(self) -> Set[InvokableUserCommand]: """Set[:class:`InvokableUserCommand`]: A set of all user commands the bot has.""" - return set(self.all_user_commands.values()) + return { + command + for meta, command in self._all_app_commands + if meta.type is ApplicationCommandType.user + } @property def message_commands(self) -> Set[InvokableMessageCommand]: """Set[:class:`InvokableMessageCommand`]: A set of all message commands the bot has.""" - return set(self.all_message_commands.values()) + return { + command + for meta, command in self._all_app_commands + if meta.type is ApplicationCommandType.message + } + + def add_app_command(self, command: InvokableApplicationCommand) -> None: + ... def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: """Adds an :class:`InvokableSlashCommand` into the internal list of slash commands. @@ -222,11 +246,30 @@ def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: if not isinstance(slash_command, InvokableSlashCommand): raise TypeError("The slash_command passed must be an instance of InvokableSlashCommand") - if slash_command.name in self.all_slash_commands: - raise CommandRegistrationError(slash_command.name) - slash_command.body.localize(self.i18n) - self.all_slash_commands[slash_command.name] = slash_command + if slash_command.guild_ids: + for guild_id in slash_command.guild_ids: + if disnake.utils.get( + self._all_app_commands, + name=slash_command.name, + type=slash_command.type, + guild_id=guild_id, + ): + raise CommandRegistrationError(slash_command.name, guild_id=guild_id) + self._all_app_commands[ + AppCommandMetadata(slash_command.name, guild_id, slash_command.type) + ] = slash_command + else: + if disnake.utils.get( + self._all_app_commands, + name=slash_command.name, + type=slash_command.type, + guild_id=None, + ): + raise CommandRegistrationError(slash_command.name) + self._all_app_commands[ + AppCommandMetadata(slash_command.name, None, slash_command.type) + ] = slash_command def add_user_command(self, user_command: InvokableUserCommand) -> None: """Adds an :class:`InvokableUserCommand` into the internal list of user commands. @@ -252,11 +295,30 @@ def add_user_command(self, user_command: InvokableUserCommand) -> None: if not isinstance(user_command, InvokableUserCommand): raise TypeError("The user_command passed must be an instance of InvokableUserCommand") - if user_command.name in self.all_user_commands: - raise CommandRegistrationError(user_command.name) - user_command.body.localize(self.i18n) - self.all_user_commands[user_command.name] = user_command + if user_command.guild_ids: + for guild_id in user_command.guild_ids: + if disnake.utils.get( + self._all_app_commands, + name=user_command.name, + type=user_command.type, + guild_id=guild_id, + ): + raise CommandRegistrationError(user_command.name, guild_id=guild_id) + self._all_app_commands[ + AppCommandMetadata(user_command.name, guild_id, user_command.type) + ] = user_command + else: + if disnake.utils.get( + self._all_app_commands, + name=user_command.name, + type=user_command.type, + guild_id=None, + ): + raise CommandRegistrationError(user_command.name) + self._all_app_commands[ + AppCommandMetadata(user_command.name, None, user_command.type) + ] = user_command def add_message_command(self, message_command: InvokableMessageCommand) -> None: """Adds an :class:`InvokableMessageCommand` into the internal list of message commands. @@ -284,13 +346,34 @@ def add_message_command(self, message_command: InvokableMessageCommand) -> None: "The message_command passed must be an instance of InvokableMessageCommand" ) - if message_command.name in self.all_message_commands: - raise CommandRegistrationError(message_command.name) - message_command.body.localize(self.i18n) - self.all_message_commands[message_command.name] = message_command - - def remove_slash_command(self, name: str) -> Optional[InvokableSlashCommand]: + if message_command.guild_ids: + for guild_id in message_command.guild_ids: + if disnake.utils.get( + self._all_app_commands, + name=message_command.name, + type=message_command.type, + guild_id=guild_id, + ): + raise CommandRegistrationError(message_command.name, guild_id=guild_id) + self._all_app_commands[ + AppCommandMetadata(message_command.name, guild_id, message_command.type) + ] = message_command + else: + if disnake.utils.get( + self._all_app_commands, + name=message_command.name, + type=message_command.type, + guild_id=None, + ): + raise CommandRegistrationError(message_command.name) + self._all_app_commands[ + AppCommandMetadata(message_command.name, None, message_command.type) + ] = message_command + + def remove_slash_command( + self, name: str, *, guild_id: int = None + ) -> Optional[InvokableSlashCommand]: """Removes an :class:`InvokableSlashCommand` from the internal list of slash commands. @@ -304,12 +387,22 @@ def remove_slash_command(self, name: str) -> Optional[InvokableSlashCommand]: Optional[:class:`InvokableSlashCommand`] The slash command that was removed. If the name is not valid then ``None`` is returned instead. """ - command = self.all_slash_commands.pop(name, None) + if guild_id: + meta = AppCommandMetadata( + name=name, type=ApplicationCommandType.chat_input, guild_id=guild_id + ) + else: + meta = AppCommandMetadata( + name=name, guild_id=None, type=ApplicationCommandType.chat_input + ) + command: InvokableSlashCommand = self._all_app_commands.pop(meta, None) # type: ignore if command is None: return None return command - def remove_user_command(self, name: str) -> Optional[InvokableUserCommand]: + def remove_user_command( + self, name: str, *, guild_id: int = None + ) -> Optional[InvokableUserCommand]: """Removes an :class:`InvokableUserCommand` from the internal list of user commands. @@ -323,12 +416,15 @@ def remove_user_command(self, name: str) -> Optional[InvokableUserCommand]: Optional[:class:`InvokableUserCommand`] The user command that was removed. If the name is not valid then ``None`` is returned instead. """ - command = self.all_user_commands.pop(name, None) + meta = AppCommandMetadata(name=name, type=ApplicationCommandType.user, guild_id=guild_id) + command: InvokableUserCommand = self._all_app_commands.pop(meta, None) # type: ignore if command is None: return None return command - def remove_message_command(self, name: str) -> Optional[InvokableMessageCommand]: + def remove_message_command( + self, name: str, *, guild_id: int = None + ) -> Optional[InvokableMessageCommand]: """Removes an :class:`InvokableMessageCommand` from the internal list of message commands. @@ -342,13 +438,43 @@ def remove_message_command(self, name: str) -> Optional[InvokableMessageCommand] Optional[:class:`InvokableMessageCommand`] The message command that was removed. If the name is not valid then ``None`` is returned instead. """ - command = self.all_message_commands.pop(name, None) + meta = AppCommandMetadata(name=name, guild_id=guild_id, type=ApplicationCommandType.message) + command: InvokableMessageCommand = self._all_app_commands.pop(meta, None) # type: ignore + if command is None: + return None + return command + + @overload + def get_app_command( + self, name: str, type: Literal[ApplicationCommandType.chat_input], *, guild_id: int = None + ) -> Optional[InvokableSlashCommand]: + ... + + @overload + def get_app_command( + self, name: str, type: Literal[ApplicationCommandType.message], *, guild_id: int = None + ) -> Optional[InvokableMessageCommand]: + ... + + @overload + def get_app_command( + self, name: str, type: Literal[ApplicationCommandType.user], *, guild_id: int = None + ) -> Optional[InvokableUserCommand]: + ... + + def get_app_command( + self, name: str, type: ApplicationCommandType, *, guild_id: int = None + ) -> Optional[InvokableApplicationCommand]: + # this does not get commands by ID, use (some other method) to do that + if not isinstance(name, str): + raise TypeError(f"Expected name to be str, not {name.__class__}") + command = self._all_app_commands.get(AppCommandMetadata(chain[0], type=type, guild_id=guild_id)) # type: ignore if command is None: return None return command def get_slash_command( - self, name: str + self, name: str, *, guild_id: int = None ) -> Optional[Union[InvokableSlashCommand, SubCommandGroup, SubCommand]]: """Works like ``Bot.get_command``, but for slash commands. @@ -376,7 +502,7 @@ def get_slash_command( raise TypeError(f"Expected name to be str, not {name.__class__}") chain = name.split() - slash = self.all_slash_commands.get(chain[0]) + slash = self.get_app_command(chain[0], ApplicationCommandType.chat_input, guild_id=guild_id) if slash is None: return None @@ -389,7 +515,9 @@ def get_slash_command( if isinstance(group, SubCommandGroup): return group.children.get(chain[2]) - def get_user_command(self, name: str) -> Optional[InvokableUserCommand]: + def get_user_command( + self, name: str, *, guild_id: int = None + ) -> Optional[InvokableUserCommand]: """Gets an :class:`InvokableUserCommand` from the internal list of user commands. @@ -403,9 +531,11 @@ def get_user_command(self, name: str) -> Optional[InvokableUserCommand]: Optional[:class:`InvokableUserCommand`] The user command that was requested. If not found, returns ``None``. """ - return self.all_user_commands.get(name) + return self.get_app_command(name, ApplicationCommandType.user, guild_id=guild_id) - def get_message_command(self, name: str) -> Optional[InvokableMessageCommand]: + def get_message_command( + self, name: str, *, guild_id: int = None + ) -> Optional[InvokableMessageCommand]: """Gets an :class:`InvokableMessageCommand` from the internal list of message commands. @@ -419,7 +549,7 @@ def get_message_command(self, name: str) -> Optional[InvokableMessageCommand]: Optional[:class:`InvokableMessageCommand`] The message command that was requested. If not found, returns ``None``. """ - return self.all_message_commands.get(name) + return self.get_app_command(name, ApplicationCommandType.message, guild_id=guild_id) def slash_command( self, @@ -810,15 +940,29 @@ def _fill_app_command_ids(self): if not isinstance(self, disnake.Client): raise NotImplementedError("This method is only usable in disnake.Client subclasses") - all_commands = self.all_slash_commands.values() + all_commands = self._all_app_commands for api_command in self._connection._global_application_commands.values(): - cmd = disnake.utils.get(all_commands, name=api_command.name) - if not cmd: + cmdmeta = disnake.utils.get(all_commands, name=api_command.name, guild_id=None) + if not cmdmeta: # consider logging continue + cmd = all_commands[cmdmeta] # todo: think about body usage if cmd.body.id is None: cmd.body.id = api_command.id + # also fill guild commands + for guild_id, api_commands in ( + (x, y.values()) for x, y in self._connection._guild_application_commands.items() + ): + + for api_command in api_commands: + cmdmeta = disnake.utils.get(all_commands, name=api_command.name, guild_id=guild_id) + if not cmdmeta: + # consider logging + continue + cmd = all_commands[cmdmeta] + if cmd.body.id is None: + cmd.body.id = api_command.id async def _delayed_command_sync(self) -> None: if not isinstance(self, disnake.Client): @@ -837,6 +981,7 @@ async def _delayed_command_sync(self) -> None: self._sync_queued = True await asyncio.sleep(2) await self._sync_application_commands() + self._fill_app_command_ids() self._sync_queued = False def _schedule_app_command_preparation(self) -> None: @@ -1216,7 +1361,13 @@ async def process_app_command_autocompletion( inter: :class:`disnake.ApplicationCommandInteraction` The interaction to process. """ - slash_command = self.all_slash_commands.get(inter.data.name) + slash_command: InvokableSlashCommand = self._all_app_commands.get( + AppCommandMetadata( + name=inter.data.name, + type=ApplicationCommandType.chat_input, + guild_id=inter.guild_id, + ) + ) if slash_command is None: return @@ -1270,24 +1421,29 @@ async def process_application_commands( return command_type = interaction.data.type - command_name = interaction.data.name app_command = None event_name = None + for command in self._all_app_commands.values(): + if command.body.id == interaction.data.id: + app_command = command + break + elif command.body.id is None: + str() + else: + app_command = None + if command_type is ApplicationCommandType.chat_input: - app_command = self.all_slash_commands.get(command_name) event_name = "slash_command" elif command_type is ApplicationCommandType.user: - app_command = self.all_user_commands.get(command_name) event_name = "user_command" elif command_type is ApplicationCommandType.message: - app_command = self.all_message_commands.get(command_name) event_name = "message_command" if event_name is None or app_command is None: - # If we are here, the command being invoked is either unknown or has an unknonw type. + # If we are here, the command being invoked is either unknown or has an unknown type. # This usually happens if the auto sync is disabled, so let's just ignore this. return From 9230d8e58db2f65ddbf72c1e20485ac608030681 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 27 Jul 2022 12:59:27 -0400 Subject: [PATCH 09/18] "fix" pyright issues --- disnake/ext/commands/interaction_bot_base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index fc657cde00..51ab3270f7 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -195,27 +195,27 @@ def application_commands(self) -> Set[InvokableApplicationCommand]: @property def slash_commands(self) -> Set[InvokableSlashCommand]: """Set[:class:`InvokableSlashCommand`]: A set of all slash commands the bot has.""" - return { + return { # type: ignore # this will always be a slash command command - for meta, command in self._all_app_commands + for meta, command in self._all_app_commands.items() if meta.type is ApplicationCommandType.chat_input } @property def user_commands(self) -> Set[InvokableUserCommand]: """Set[:class:`InvokableUserCommand`]: A set of all user commands the bot has.""" - return { + return { # type: ignore # this will always be a user command command - for meta, command in self._all_app_commands + for meta, command in self._all_app_commands.items() if meta.type is ApplicationCommandType.user } @property def message_commands(self) -> Set[InvokableMessageCommand]: """Set[:class:`InvokableMessageCommand`]: A set of all message commands the bot has.""" - return { + return { # type: ignore # this will always be a message command command - for meta, command in self._all_app_commands + for meta, command in self._all_app_commands.items() if meta.type is ApplicationCommandType.message } @@ -1361,7 +1361,7 @@ async def process_app_command_autocompletion( inter: :class:`disnake.ApplicationCommandInteraction` The interaction to process. """ - slash_command: InvokableSlashCommand = self._all_app_commands.get( + slash_command: InvokableSlashCommand = self._all_app_commands.get( # type: ignore :3 AppCommandMetadata( name=inter.data.name, type=ApplicationCommandType.chat_input, From fe9d277015b6052e6f900c084f95e8f972149541 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 27 Jul 2022 13:05:24 -0400 Subject: [PATCH 10/18] fix: sync command ids when bulk updating guild slash commands --- disnake/client.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/disnake/client.py b/disnake/client.py index 1cd31b7329..99903dabdb 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -2647,7 +2647,16 @@ async def bulk_overwrite_guild_commands( """ for cmd in application_commands: cmd.localize(self.i18n) - return await self._connection.bulk_overwrite_guild_commands(guild_id, application_commands) + res = await self._connection.bulk_overwrite_guild_commands(guild_id, application_commands) + + for api_command in res: + cmd = utils.get(application_commands, name=api_command.name) + if not cmd: + # consider a warning + continue + cmd.id = api_command.id + + return res # Application command permissions @@ -2660,6 +2669,10 @@ async def bulk_fetch_command_permissions( .. versionadded:: 2.1 + .. versionchanged:: 2.6 + + Modifies the commands' ``id`` attribute to correspond to the version on the API. + Parameters ---------- guild_id: :class:`int` From ec79472b13a0a66142a490ccb82766b2faed02a4 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 28 Jul 2022 02:51:58 -0400 Subject: [PATCH 11/18] chore: add parents properties --- disnake/ext/commands/slash_core.py | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index b443a460a5..f3c1a0a80e 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -194,6 +194,20 @@ def qualified_name(self) -> str: def parent(self) -> Optional[InvokableSlashCommand]: return self._parent + @property + def parents( + self, + ) -> Tuple[InvokableSlashCommand]: + """Tuple[:class:`InvokableSlashCommand`]: Retrieves the parents of this command. + + If the command has no parents then it returns an empty :class:`tuple`. + + For example in commands ``/a b test``, the parents are ``(b, a)``. + + .. versionadded:: 2.6 + """ + return (self.parent,) # type: ignore + def sub_command( self, name: LocalizedOptional = None, @@ -360,6 +374,26 @@ def qualified_name(self) -> str: def parent(self) -> Optional[Union[InvokableSlashCommand, SubCommandGroup]]: return self._parent + @property + def parents( + self, + ) -> Union[Tuple[InvokableSlashCommand], Tuple[SubCommandGroup, InvokableSlashCommand]]: + """Union[Tuple[:class:`InvokableSlashCommand`], Tuple[:class:`SubCommandGroup`, :class:`InvokableSlashCommand`]]: Retrieves the parents of this command. + + If the command has no parents then it returns an empty :class:`tuple`. + + For example in commands ``/a b test``, the parents are ``(b, a)``. + + .. versionadded:: 2.6 + """ + entries = [] + command = self + while command.parent is not None: # type: ignore + command = command.parent # type: ignore + entries.append(command) + + return tuple(entries) + async def _call_autocompleter( self, param: str, inter: ApplicationCommandInteraction, user_input: str ) -> Optional[Choices]: From e5c619baf884c8606dca41d7aab64ea40ad7caef Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 28 Jul 2022 02:52:12 -0400 Subject: [PATCH 12/18] feat: add slash command mentions --- disnake/ext/commands/base_core.py | 5 +++++ disnake/ext/commands/slash_core.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 8d66387cb2..a6aabcbad4 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -252,6 +252,11 @@ def dm_permission(self) -> bool: def qualified_name(self) -> str: return self.name + @property + def mention(self) -> str: + # todo: add docs and make ID non-nullable + return f"" + @property def default_member_permissions(self) -> Optional[Permissions]: """Optional[:class:`.Permissions`]: The default required member permissions for this command. diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index f3c1a0a80e..d6b84ca9af 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -190,6 +190,12 @@ def body(self) -> Option: def qualified_name(self) -> str: return f"{self._parent.qualified_name} {self.name}" + @property + def mention(self) -> str: + # todo: add docs and make ID non-nullable + return f"" + + # todo: refactor this class to make this not optional @property def parent(self) -> Optional[InvokableSlashCommand]: return self._parent @@ -370,6 +376,11 @@ def qualified_name(self) -> str: return self.name return f"{self._parent.qualified_name} {self.name}" + @property + def mention(self) -> str: + # todo: add docs and make ID non-nullable + return f"" + @property def parent(self) -> Optional[Union[InvokableSlashCommand, SubCommandGroup]]: return self._parent From 7cec79ee9b4e664df1a31bb01cbd95f9290f7862 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sun, 14 Aug 2022 02:57:03 -0400 Subject: [PATCH 13/18] fix some bugs --- disnake/ext/commands/interaction_bot_base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 51ab3270f7..b445cd701a 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -7,7 +7,6 @@ import sys import traceback import warnings -from itertools import chain from typing import ( TYPE_CHECKING, Any, @@ -468,7 +467,7 @@ def get_app_command( # this does not get commands by ID, use (some other method) to do that if not isinstance(name, str): raise TypeError(f"Expected name to be str, not {name.__class__}") - command = self._all_app_commands.get(AppCommandMetadata(chain[0], type=type, guild_id=guild_id)) # type: ignore + command = self._all_app_commands.get(AppCommandMetadata(name, type=type, guild_id=guild_id)) if command is None: return None return command @@ -884,7 +883,7 @@ async def _sync_application_commands(self) -> None: for guild_id, cmds in guild_cmds.items(): current_guild_cmds = self._connection._guild_application_commands.get(guild_id, {}) diff = _app_commands_diff(cmds, current_guild_cmds.values()) - if self._command_sync.allow_command_deletion: + if not self._command_sync.allow_command_deletion: # because allow_command_deletion is disabled, we want to never delete a command, so we move the delete commands to no_changes diff["no_changes"] += diff["delete"] diff["delete"].clear() @@ -1429,7 +1428,7 @@ async def process_application_commands( app_command = command break elif command.body.id is None: - str() + ... else: app_command = None From 6468099af7d416d08d7a4ccea9591c6fe94bc9e2 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 7 Sep 2022 21:08:19 -0400 Subject: [PATCH 14/18] chore: remove reassigning imported attributes --- disnake/ext/commands/slash_core.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index d6b84ca9af..7dd9976eeb 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -598,10 +598,6 @@ def name_localizations(self) -> LocalizationValue: def description_localizations(self) -> LocalizationValue: return self._description_localised.localizations - add_option = SlashCommand.add_option - to_dict = SlashCommand.to_dict - localize = SlashCommand.localize - def sub_command( self, name: LocalizedOptional = None, From 9695975e00d44094c09764c1da77b052065e9995 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 7 Sep 2022 22:04:25 -0400 Subject: [PATCH 15/18] remove removed parameter documentation --- disnake/ext/commands/bot.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/disnake/ext/commands/bot.py b/disnake/ext/commands/bot.py index f685ac00df..eaca344d58 100644 --- a/disnake/ext/commands/bot.py +++ b/disnake/ext/commands/bot.py @@ -26,6 +26,7 @@ from ._types import MaybeCoro from .bot_base import PrefixType + from .flags import ApplicationCommandSyncFlags from .help import HelpCommand @@ -221,9 +222,7 @@ def __init__( owner_ids: Optional[Set[int]] = None, reload: bool = False, case_insensitive: bool = False, - sync_commands: bool = True, - sync_commands_debug: bool = False, - sync_commands_on_cog_unload: bool = True, + command_sync: ApplicationCommandSyncFlags = ..., test_guilds: Optional[Sequence[int]] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -272,9 +271,7 @@ def __init__( owner_ids: Optional[Set[int]] = None, reload: bool = False, case_insensitive: bool = False, - sync_commands: bool = True, - sync_commands_debug: bool = False, - sync_commands_on_cog_unload: bool = True, + command_sync: ApplicationCommandSyncFlags = ..., test_guilds: Optional[Sequence[int]] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -404,9 +401,7 @@ def __init__( owner_id: Optional[int] = None, owner_ids: Optional[Set[int]] = None, reload: bool = False, - sync_commands: bool = True, - sync_commands_debug: bool = False, - sync_commands_on_cog_unload: bool = True, + command_sync: ApplicationCommandSyncFlags = ..., test_guilds: Optional[Sequence[int]] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -448,9 +443,7 @@ def __init__( owner_id: Optional[int] = None, owner_ids: Optional[Set[int]] = None, reload: bool = False, - sync_commands: bool = True, - sync_commands_debug: bool = False, - sync_commands_on_cog_unload: bool = True, + command_sync: ApplicationCommandSyncFlags = ..., test_guilds: Optional[Sequence[int]] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, From 7eda7e4b91a2ef3e54371dea602dba96acee4980 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sat, 1 Oct 2022 00:46:34 -0400 Subject: [PATCH 16/18] chore: resolve lint and typing issues --- disnake/app_commands.py | 8 +-- disnake/ext/commands/errors.py | 4 +- disnake/ext/commands/flags.py | 29 +-------- disnake/ext/commands/interaction_bot_base.py | 38 +++++++----- disnake/ext/commands/slash_core.py | 62 ++++++-------------- test_bot/__main__.py | 2 +- 6 files changed, 54 insertions(+), 89 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index b92c7a0b74..a276f601da 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -468,7 +468,7 @@ def __init__( name: LocalizedRequired, dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, - id: int = None, + id: Optional[int] = None, ): self.type: ApplicationCommandType = enum_if_int(ApplicationCommandType, type) @@ -605,7 +605,7 @@ def __init__( name: LocalizedRequired, dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, - id: int = None, + id: Optional[int] = None, ): super().__init__( type=ApplicationCommandType.user, @@ -693,7 +693,7 @@ def __init__( name: LocalizedRequired, dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, - id: int = None, + id: Optional[int] = None, ): super().__init__( type=ApplicationCommandType.message, @@ -799,7 +799,7 @@ def __init__( options: Optional[List[Option]] = None, dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, - id: int = None, + id: Optional[int] = None, ): super().__init__( type=ApplicationCommandType.chat_input, diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index 7bbdf53d55..9d783a4e4f 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -1016,7 +1016,9 @@ class CommandRegistrationError(ClientException): Whether the name that conflicts is an alias of the command we try to add. """ - def __init__(self, name: str, *, alias_conflict: bool = False, guild_id: int = None) -> None: + def __init__( + self, name: str, *, alias_conflict: bool = False, guild_id: Optional[int] = None + ) -> None: self.name: str = name self.alias_conflict: bool = alias_conflict self.guild_id: Optional[int] = guild_id diff --git a/disnake/ext/commands/flags.py b/disnake/ext/commands/flags.py index ae51d7bf32..b3f4d260bc 100644 --- a/disnake/ext/commands/flags.py +++ b/disnake/ext/commands/flags.py @@ -1,32 +1,10 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-2021 Rapptz -Copyright (c) 2021-present Disnake Development - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" +# SPDX-License-Identifier: MIT + from __future__ import annotations from typing import TYPE_CHECKING -from disnake.flags import BaseFlags, alias_flag_value, all_flags_value, fill_with_flags, flag_value +from disnake.flags import BaseFlags, alias_flag_value, all_flags_value, flag_value if TYPE_CHECKING: from typing_extensions import Self @@ -34,7 +12,6 @@ __all__ = ("ApplicationCommandSyncFlags",) -@fill_with_flags() class ApplicationCommandSyncFlags(BaseFlags): """Controls the library's application command syncing policy. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index b445cd701a..c8672bc009 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -142,7 +142,7 @@ class InteractionBotBase(CommonBotBase): def __init__( self, *, - command_sync: ApplicationCommandSyncFlags = None, + command_sync: Optional[ApplicationCommandSyncFlags] = None, test_guilds: Optional[Sequence[int]] = None, **options: Any, ): @@ -178,9 +178,7 @@ def __init__( self._schedule_app_command_preparation() def application_commands_iterator(self) -> Iterable[InvokableApplicationCommand]: - return chain( - self._all_app_commands.values(), - ) + yield from self._all_app_commands.values() @property def all_app_commands(self) -> List[InvokableApplicationCommand]: @@ -371,7 +369,7 @@ def add_message_command(self, message_command: InvokableMessageCommand) -> None: ] = message_command def remove_slash_command( - self, name: str, *, guild_id: int = None + self, name: str, *, guild_id: Optional[int] = None ) -> Optional[InvokableSlashCommand]: """Removes an :class:`InvokableSlashCommand` from the internal list of slash commands. @@ -400,7 +398,7 @@ def remove_slash_command( return command def remove_user_command( - self, name: str, *, guild_id: int = None + self, name: str, *, guild_id: Optional[int] = None ) -> Optional[InvokableUserCommand]: """Removes an :class:`InvokableUserCommand` from the internal list of user commands. @@ -422,7 +420,7 @@ def remove_user_command( return command def remove_message_command( - self, name: str, *, guild_id: int = None + self, name: str, *, guild_id: Optional[int] = None ) -> Optional[InvokableMessageCommand]: """Removes an :class:`InvokableMessageCommand` from the internal list of message commands. @@ -445,24 +443,36 @@ def remove_message_command( @overload def get_app_command( - self, name: str, type: Literal[ApplicationCommandType.chat_input], *, guild_id: int = None + self, + name: str, + type: Literal[ApplicationCommandType.chat_input], + *, + guild_id: Optional[int] = None, ) -> Optional[InvokableSlashCommand]: ... @overload def get_app_command( - self, name: str, type: Literal[ApplicationCommandType.message], *, guild_id: int = None + self, + name: str, + type: Literal[ApplicationCommandType.message], + *, + guild_id: Optional[int] = None, ) -> Optional[InvokableMessageCommand]: ... @overload def get_app_command( - self, name: str, type: Literal[ApplicationCommandType.user], *, guild_id: int = None + self, + name: str, + type: Literal[ApplicationCommandType.user], + *, + guild_id: Optional[int] = None, ) -> Optional[InvokableUserCommand]: ... def get_app_command( - self, name: str, type: ApplicationCommandType, *, guild_id: int = None + self, name: str, type: ApplicationCommandType, *, guild_id: Optional[int] = None ) -> Optional[InvokableApplicationCommand]: # this does not get commands by ID, use (some other method) to do that if not isinstance(name, str): @@ -473,7 +483,7 @@ def get_app_command( return command def get_slash_command( - self, name: str, *, guild_id: int = None + self, name: str, *, guild_id: Optional[int] = None ) -> Optional[Union[InvokableSlashCommand, SubCommandGroup, SubCommand]]: """Works like ``Bot.get_command``, but for slash commands. @@ -515,7 +525,7 @@ def get_slash_command( return group.children.get(chain[2]) def get_user_command( - self, name: str, *, guild_id: int = None + self, name: str, *, guild_id: Optional[int] = None ) -> Optional[InvokableUserCommand]: """Gets an :class:`InvokableUserCommand` from the internal list of user commands. @@ -533,7 +543,7 @@ def get_user_command( return self.get_app_command(name, ApplicationCommandType.user, guild_id=guild_id) def get_message_command( - self, name: str, *, guild_id: int = None + self, name: str, *, guild_id: Optional[int] = None ) -> Optional[InvokableMessageCommand]: """Gets an :class:`InvokableMessageCommand` from the internal list of message commands. diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index 7dd9976eeb..f284e6313a 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -176,7 +176,11 @@ def root_parent(self) -> InvokableSlashCommand: @property def parents(self) -> Tuple[InvokableSlashCommand]: - """Tuple[:class:`InvokableSlashCommand`]: Returns all parents of this group. + """Tuple[:class:`InvokableSlashCommand`]: Retrieves the parents of this command. + + If the command has no parents then it returns an empty :class:`tuple`. + + For example in commands ``/a b test``, the parents are ``(b, a)``. .. versionadded:: 2.6 """ @@ -195,25 +199,10 @@ def mention(self) -> str: # todo: add docs and make ID non-nullable return f"" - # todo: refactor this class to make this not optional @property - def parent(self) -> Optional[InvokableSlashCommand]: + def parent(self) -> InvokableSlashCommand: return self._parent - @property - def parents( - self, - ) -> Tuple[InvokableSlashCommand]: - """Tuple[:class:`InvokableSlashCommand`]: Retrieves the parents of this command. - - If the command has no parents then it returns an empty :class:`tuple`. - - For example in commands ``/a b test``, the parents are ``(b, a)``. - - .. versionadded:: 2.6 - """ - return (self.parent,) # type: ignore - def sub_command( self, name: LocalizedOptional = None, @@ -344,7 +333,7 @@ def root_parent(self) -> InvokableSlashCommand: .. versionadded:: 2.6 """ - return self.parent.parent if isinstance(self.parent, SubCommandGroup) else self.parent + return self._parent._parent if isinstance(self._parent, SubCommandGroup) else self._parent @property def parents( @@ -357,10 +346,10 @@ def parents( .. versionadded:: 2.6 """ - # here I'm not using 'self.parent.parents + (self.parent,)' because it causes typing issues - if isinstance(self.parent, SubCommandGroup): - return (self.parent, self.parent.parent) - return (self.parent,) + # here I'm not using 'self._parent._parents + (self._parent,)' because it causes typing issues + if isinstance(self._parent, SubCommandGroup): + return (self._parent, self._parent._parent) + return (self._parent,) @property def description(self) -> str: @@ -382,29 +371,9 @@ def mention(self) -> str: return f"" @property - def parent(self) -> Optional[Union[InvokableSlashCommand, SubCommandGroup]]: + def parent(self) -> Union[InvokableSlashCommand, SubCommandGroup]: return self._parent - @property - def parents( - self, - ) -> Union[Tuple[InvokableSlashCommand], Tuple[SubCommandGroup, InvokableSlashCommand]]: - """Union[Tuple[:class:`InvokableSlashCommand`], Tuple[:class:`SubCommandGroup`, :class:`InvokableSlashCommand`]]: Retrieves the parents of this command. - - If the command has no parents then it returns an empty :class:`tuple`. - - For example in commands ``/a b test``, the parents are ``(b, a)``. - - .. versionadded:: 2.6 - """ - entries = [] - command = self - while command.parent is not None: # type: ignore - command = command.parent # type: ignore - entries.append(command) - - return tuple(entries) - async def _call_autocompleter( self, param: str, inter: ApplicationCommandInteraction, user_input: str ) -> Optional[Choices]: @@ -560,6 +529,13 @@ def parents(self) -> Tuple[()]: """ return () + @property + def parent(self) -> None: + """``None``: This is for consistency with :class:`SubCommand` and :class:`SubCommandGroup`. + + .. versionadded:: 2.6 + """ + def _ensure_assignment_on_copy(self, other: SlashCommandT) -> SlashCommandT: super()._ensure_assignment_on_copy(other) if self.connectors != other.connectors: diff --git a/test_bot/__main__.py b/test_bot/__main__.py index 5e084fdcb3..3e4e7babae 100644 --- a/test_bot/__main__.py +++ b/test_bot/__main__.py @@ -32,7 +32,7 @@ def __init__(self): command_prefix=Config.prefix, intents=disnake.Intents.all(), help_command=None, # type: ignore - sync_commands_debug=Config.sync_commands_debug, + command_sync=commands.ApplicationCommandSyncFlags.all(), strict_localization=Config.strict_localization, test_guilds=Config.test_guilds, reload=Config.auto_reload, From e824ed8ce78f0e695ce7f125d094ea6b94b197b2 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 21 Oct 2022 00:31:26 -0400 Subject: [PATCH 17/18] Revert "feat: move command sync config to flags" This reverts commit 237ac58f952ab9542403bedd130d3939ea7769a6. --- disnake/ext/commands/__init__.py | 1 - disnake/ext/commands/bot.py | 5 ----- disnake/ext/commands/cog.py | 4 ++-- disnake/ext/commands/interaction_bot_base.py | 20 ++++++++++---------- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/disnake/ext/commands/__init__.py b/disnake/ext/commands/__init__.py index 6a59dde1c9..c97b3a0ebf 100644 --- a/disnake/ext/commands/__init__.py +++ b/disnake/ext/commands/__init__.py @@ -21,7 +21,6 @@ from .custom_warnings import * from .errors import * from .flag_converter import * -from .flags import * from .help import * from .params import * from .slash_core import * diff --git a/disnake/ext/commands/bot.py b/disnake/ext/commands/bot.py index eaca344d58..73230a00a5 100644 --- a/disnake/ext/commands/bot.py +++ b/disnake/ext/commands/bot.py @@ -62,11 +62,6 @@ class Bot(BotBase, InteractionBotBase, disnake.Client): .. versionadded:: 2.1 - command_sync: :class:`ApplicationCommandSyncFlags` - The configuration for application command sync. - - .. versionadded:: 2.6 - sync_commands: :class:`bool` Whether to enable automatic synchronization of application commands in your code. Defaults to ``True``, which means that commands in API are automatically synced diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py index b493edf685..54b33e9c55 100644 --- a/disnake/ext/commands/cog.py +++ b/disnake/ext/commands/cog.py @@ -808,7 +808,7 @@ def _inject(self, bot: AnyBot) -> Self: bot.add_listener(getattr(self, method_name), name) try: - if bot._command_sync.on_cog_unload: + if bot._sync_commands_on_cog_unload: bot._schedule_delayed_command_sync() except NotImplementedError: pass @@ -874,7 +874,7 @@ def _eject(self, bot: AnyBot) -> None: finally: try: - if bot._command_sync.on_cog_unload: + if bot._sync_commands_on_cog_unload: bot._schedule_delayed_command_sync() except NotImplementedError: pass diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index c8672bc009..b5c7deca57 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -41,7 +41,6 @@ user_command, ) from .errors import CommandRegistrationError -from .flags import ApplicationCommandSyncFlags from .slash_core import InvokableSlashCommand, SubCommand, SubCommandGroup, slash_command if TYPE_CHECKING: @@ -142,7 +141,9 @@ class InteractionBotBase(CommonBotBase): def __init__( self, *, - command_sync: Optional[ApplicationCommandSyncFlags] = None, + sync_commands: bool = True, + sync_commands_debug: bool = False, + sync_commands_on_cog_unload: bool = True, test_guilds: Optional[Sequence[int]] = None, **options: Any, ): @@ -153,11 +154,10 @@ def __init__( test_guilds = None if test_guilds is None else tuple(test_guilds) self._test_guilds: Optional[Tuple[int, ...]] = test_guilds - if command_sync is None: - command_sync = ApplicationCommandSyncFlags.default() - + self._sync_commands: bool = sync_commands + self._sync_commands_debug: bool = sync_commands_debug + self._sync_commands_on_cog_unload = sync_commands_on_cog_unload self._sync_queued: bool = False - self._command_sync = command_sync self._slash_command_checks = [] self._slash_command_check_once = [] @@ -853,7 +853,7 @@ async def _sync_application_commands(self) -> None: if not isinstance(self, disnake.Client): raise NotImplementedError("This method is only usable in disnake.Client subclasses") - if not self._command_sync.sync_commands or self._is_closed or self.loop.is_closed(): + if not self._sync_commands or self._is_closed or self.loop.is_closed(): return # We assume that all commands are already cached. @@ -921,7 +921,7 @@ async def _sync_application_commands(self) -> None: self._log_sync_debug("Command synchronization task has finished") def _log_sync_debug(self, text: str) -> None: - if self._command_sync.sync_commands_debug: + if self._sync_commands_debug: # if sync debugging is enabled, *always* output logs if _log.isEnabledFor(logging.INFO): # if the log level is `INFO` or higher, use that @@ -978,7 +978,7 @@ async def _delayed_command_sync(self) -> None: raise NotImplementedError("This method is only usable in disnake.Client subclasses") if ( - not self._command_sync.sync_commands + not self._sync_commands or self._sync_queued or not self.is_ready() or self._is_closed @@ -1403,7 +1403,7 @@ async def process_application_commands( interaction: :class:`disnake.ApplicationCommandInteraction` The interaction to process commands for. """ - if self._command_sync.sync_commands and not self._sync_queued: + if self._sync_commands and not self._sync_queued: known_command = self.get_global_command(interaction.data.id) # type: ignore if known_command is None: From 71151952caf2f78406080aad68070668dd276aff Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 21 Oct 2022 00:32:39 -0400 Subject: [PATCH 18/18] chore: remove other parts of command sync --- disnake/ext/commands/bot.py | 5 - disnake/ext/commands/flags.py | 150 ------------------- disnake/ext/commands/interaction_bot_base.py | 12 +- test_bot/__main__.py | 2 +- 4 files changed, 3 insertions(+), 166 deletions(-) delete mode 100644 disnake/ext/commands/flags.py diff --git a/disnake/ext/commands/bot.py b/disnake/ext/commands/bot.py index 73230a00a5..e27e1abe69 100644 --- a/disnake/ext/commands/bot.py +++ b/disnake/ext/commands/bot.py @@ -26,7 +26,6 @@ from ._types import MaybeCoro from .bot_base import PrefixType - from .flags import ApplicationCommandSyncFlags from .help import HelpCommand @@ -217,7 +216,6 @@ def __init__( owner_ids: Optional[Set[int]] = None, reload: bool = False, case_insensitive: bool = False, - command_sync: ApplicationCommandSyncFlags = ..., test_guilds: Optional[Sequence[int]] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -266,7 +264,6 @@ def __init__( owner_ids: Optional[Set[int]] = None, reload: bool = False, case_insensitive: bool = False, - command_sync: ApplicationCommandSyncFlags = ..., test_guilds: Optional[Sequence[int]] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -396,7 +393,6 @@ def __init__( owner_id: Optional[int] = None, owner_ids: Optional[Set[int]] = None, reload: bool = False, - command_sync: ApplicationCommandSyncFlags = ..., test_guilds: Optional[Sequence[int]] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -438,7 +434,6 @@ def __init__( owner_id: Optional[int] = None, owner_ids: Optional[Set[int]] = None, reload: bool = False, - command_sync: ApplicationCommandSyncFlags = ..., test_guilds: Optional[Sequence[int]] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, diff --git a/disnake/ext/commands/flags.py b/disnake/ext/commands/flags.py deleted file mode 100644 index b3f4d260bc..0000000000 --- a/disnake/ext/commands/flags.py +++ /dev/null @@ -1,150 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from disnake.flags import BaseFlags, alias_flag_value, all_flags_value, flag_value - -if TYPE_CHECKING: - from typing_extensions import Self - -__all__ = ("ApplicationCommandSyncFlags",) - - -class ApplicationCommandSyncFlags(BaseFlags): - """Controls the library's application command syncing policy. - - This allows for finer grained control over what commands are synced automatically and in what cases. - - To construct an object you can pass keyword arguments denoting the flags - to enable or disable. - - The default value is all flags enabled. - - .. versionadded:: 2.6 - - .. container:: operations - - .. describe:: x == y - - Checks if two ApplicationCommandSyncFlags instances are equal. - .. describe:: x != y - - Checks if two ApplicationCommandSyncFlags instances are not equal. - .. describe:: x <= y - - Checks if an ApplicationCommandSyncFlags instance is a subset of another ApplicationCommandSyncFlags instance. - .. describe:: x >= y - - Checks if an ApplicationCommandSyncFlags instance is a superset of another ApplicationCommandSyncFlags instance. - .. describe:: x < y - - Checks if an ApplicationCommandSyncFlags instance is a strict subset of another ApplicationCommandSyncFlags instance. - .. describe:: x > y - - Checks if an ApplicationCommandSyncFlags instance is a strict superset of another ApplicationCommandSyncFlags instance. - .. describe:: x | y, x |= y - - Returns a new ApplicationCommandSyncFlags instance with all enabled flags from both x and y. - (Using ``|=`` will update in place). - .. describe:: x & y, x &= y - - Returns a new ApplicationCommandSyncFlags instance with only flags enabled on both x and y. - (Using ``&=`` will update in place). - .. describe:: x ^ y, x ^= y - - Returns a new ApplicationCommandSyncFlags instance with only flags enabled on one of x or y, but not both. - (Using ``^=`` will update in place). - .. describe:: ~x - - Returns a new ApplicationCommandSyncFlags instance with all flags from x inverted. - .. describe:: hash(x) - - Return the flag's hash. - .. describe:: iter(x) - - Returns an iterator of ``(name, value)`` pairs. This allows it - to be, for example, constructed as a dict or a list of pairs. - Note that aliases are not shown. - - - Additionally supported are a few operations on class attributes. - - .. describe:: ApplicationCommandSyncFlags.y | ApplicationCommandSyncFlags.z, ApplicationCommandSyncFlags(y=True) | ApplicationCommandSyncFlags.z - - Returns a ApplicationCommandSyncFlags instance with all provided flags enabled. - - .. describe:: ~ApplicationCommandSyncFlags.y - - Returns a ApplicationCommandSyncFlags instance with all flags except ``y`` inverted from their default value. - - .. versionadded:: 2.6 - Attributes - ---------- - value: :class:`int` - The raw value. You should query flags via the properties - rather than using this raw value. - """ - - __slots__ = () - - def __init__(self, **kwargs: bool): - self.value = all_flags_value(self.VALID_FLAGS) - for key, value in kwargs.items(): - if key not in self.VALID_FLAGS: - raise TypeError(f"{key!r} is not a valid flag name.") - setattr(self, key, value) - - @classmethod - def all(cls) -> Self: - """A factory method that creates a :class:`ApplicationCommandSyncFlags` with everything enabled.""" - self = cls.__new__(cls) - self.value = all_flags_value(cls.VALID_FLAGS) - return self - - @classmethod - def none(cls) -> Self: - """A factory method that creates a :class:`ApplicationCommandSyncFlags` with everything disabled.""" - self = cls.__new__(cls) - self.value = self.DEFAULT_VALUE - return self - - @classmethod - def default(cls) -> Self: - """A factory method that creates a :class:`ApplicationCommandSyncFlags` with the default settings.""" - instance = cls.all() - instance.sync_commands_debug = False - return instance - - @alias_flag_value - def sync_commands(self): - """:class:`bool`: Whether to sync app commands at all.""" - return 1 << 5 | 1 << 6 - - @flag_value - def sync_commands_debug(self): - """:class:`bool`: Whether or not to show app command sync debug messages""" - return 1 << 1 - - @alias_flag_value - def on_cog_actions(self): - """:class:`bool`: Whether or not to sync app commands on cog load, unload, or reload.""" - return 1 << 2 | 1 << 4 - - @flag_value - def on_cog_unload(self): - """:class:`bool`: Whether or not to sync app commands on cog unload or reload.""" - return 1 << 2 - - @flag_value - def allow_command_deletion(self): - return 1 << 3 - - @flag_value - def global_commands(self): - return 1 << 5 - - @flag_value - def guild_commands(self): - return 1 << 6 diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index b5c7deca57..cab9170b87 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -860,15 +860,11 @@ async def _sync_application_commands(self) -> None: # Sort all invokable commands between guild IDs: global_cmds, guild_cmds = self._ordered_unsynced_commands(self._test_guilds) - if global_cmds is not None and self._command_sync.global_commands: + if global_cmds is not None: # Update global commands first diff = _app_commands_diff( global_cmds, self._connection._global_application_commands.values() ) - if not self._command_sync.allow_command_deletion: - # because allow_command_deletion is disabled, we want to never delete a command, so we move the delete commands to no_changes - diff["no_changes"] += diff["delete"] - diff["delete"].clear() update_required = bool(diff["upsert"]) or bool(diff["edit"]) or bool(diff["delete"]) # Show the difference @@ -889,14 +885,10 @@ async def _sync_application_commands(self) -> None: # Same process but for each specified guild individually. # Notice that we're not doing this for every single guild for optimisation purposes. # See the note in :meth:`_cache_application_commands` about guild app commands. - if guild_cmds is not None and self._command_sync.guild_commands: + if guild_cmds is not None: for guild_id, cmds in guild_cmds.items(): current_guild_cmds = self._connection._guild_application_commands.get(guild_id, {}) diff = _app_commands_diff(cmds, current_guild_cmds.values()) - if not self._command_sync.allow_command_deletion: - # because allow_command_deletion is disabled, we want to never delete a command, so we move the delete commands to no_changes - diff["no_changes"] += diff["delete"] - diff["delete"].clear() update_required = bool(diff["upsert"]) or bool(diff["edit"]) or bool(diff["delete"]) # Show diff diff --git a/test_bot/__main__.py b/test_bot/__main__.py index 3e4e7babae..5e084fdcb3 100644 --- a/test_bot/__main__.py +++ b/test_bot/__main__.py @@ -32,7 +32,7 @@ def __init__(self): command_prefix=Config.prefix, intents=disnake.Intents.all(), help_command=None, # type: ignore - command_sync=commands.ApplicationCommandSyncFlags.all(), + sync_commands_debug=Config.sync_commands_debug, strict_localization=Config.strict_localization, test_guilds=Config.test_guilds, reload=Config.auto_reload,