From 92f19f555ed192af32b761a9d7e34e1ad9909822 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Wed, 20 Mar 2024 17:09:16 +0100 Subject: [PATCH 01/36] feat(types): update/add new fields to `disnake.types` --- disnake/types/appinfo.py | 11 +++++++- disnake/types/interactions.py | 53 ++++++++++++++++++++++++++++++++--- disnake/types/message.py | 5 ++-- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/disnake/types/appinfo.py b/disnake/types/appinfo.py index 35507dc82d..785703acb5 100644 --- a/disnake/types/appinfo.py +++ b/disnake/types/appinfo.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional, TypedDict +from typing import Dict, List, Literal, Optional, TypedDict from typing_extensions import NotRequired @@ -10,6 +10,9 @@ from .team import Team from .user import User +# (also called "installation context", which seems more accurate) +ApplicationIntegrationType = Literal[0, 1] # GUILD_INSTALL, USER_INSTALL + class BaseAppInfo(TypedDict): id: Snowflake @@ -29,6 +32,10 @@ class InstallParams(TypedDict): permissions: str +class ApplicationIntegrationTypeConfig(TypedDict, total=False): + oauth2_install_params: InstallParams + + class AppInfo(BaseAppInfo): rpc_origins: List[str] bot_public: bool @@ -42,6 +49,8 @@ class AppInfo(BaseAppInfo): install_params: NotRequired[InstallParams] custom_install_url: NotRequired[str] role_connections_verification_url: NotRequired[str] + # values in this dict generally shouldn't be null, but the spec claims that, so handle it just in case + integration_types_config: NotRequired[Dict[str, Optional[ApplicationIntegrationTypeConfig]]] class PartialAppInfo(BaseAppInfo, total=False): diff --git a/disnake/types/interactions.py b/disnake/types/interactions.py index efffa8e599..2f94bf82b1 100644 --- a/disnake/types/interactions.py +++ b/disnake/types/interactions.py @@ -6,6 +6,7 @@ from typing_extensions import NotRequired +from .appinfo import ApplicationIntegrationType from .channel import ChannelType from .components import Component, Modal from .embed import Embed @@ -23,6 +24,10 @@ ApplicationCommandType = Literal[1, 2, 3] +# TODO: naming? +# to quote the notion doc, "Not limited to application command interactions. Unfortunate naming here" +InteractionContextType = Literal[1, 2, 3] # GUILD, BOT_DM, PRIVATE_CHANNEL + class ApplicationCommand(TypedDict): id: Snowflake @@ -35,9 +40,13 @@ class ApplicationCommand(TypedDict): description_localizations: NotRequired[Optional[LocalizationDict]] options: NotRequired[List[ApplicationCommandOption]] default_member_permissions: NotRequired[Optional[str]] - dm_permission: NotRequired[Optional[bool]] + dm_permission: NotRequired[Optional[bool]] # deprecated default_permission: NotRequired[bool] # deprecated nsfw: NotRequired[bool] + # TODO: according to the spec, this is also optional + nullable, though I haven't encountered that yet + integration_types: List[ApplicationIntegrationType] + # TODO: this appears to be optional + nullable + contexts: List[InteractionContextType] version: Snowflake @@ -254,6 +263,7 @@ class _BaseInteraction(TypedDict): application_id: Snowflake token: str version: Literal[1] + app_permissions: str # common properties in non-ping interactions @@ -262,10 +272,12 @@ class _BaseUserInteraction(_BaseInteraction): # but it is assumed to always exist on non-ping interactions channel_id: Snowflake locale: str - app_permissions: NotRequired[str] guild_id: NotRequired[Snowflake] guild_locale: NotRequired[str] entitlements: NotRequired[List[Entitlement]] + # keys are stringified ApplicationIntegrationType's + authorizing_integration_owners: NotRequired[Dict[str, Snowflake]] + context: NotRequired[InteractionContextType] # one of these two will always exist, according to docs member: NotRequired[MemberWithUser] user: NotRequired[User] @@ -336,6 +348,37 @@ class InteractionMessageReference(TypedDict): user: User +class _BaseInteractionMetadata(TypedDict): + id: Snowflake + type: InteractionType + user_id: Snowflake + # keys are stringified ApplicationIntegrationType's + authorizing_integration_owners: Dict[str, Snowflake] + original_response_message_id: NotRequired[Snowflake] # only on followups + + +class ApplicationCommandInteractionMetadata(_BaseInteractionMetadata): + name: NotRequired[str] # not documented + + +class MessageComponentInteractionMetadata(_BaseInteractionMetadata): + interacted_message_id: Snowflake + + +class ModalInteractionMetadata(_BaseInteractionMetadata): + triggering_interaction_metadata: Union[ + ApplicationCommandInteractionMetadata, + MessageComponentInteractionMetadata, + ] + + +InteractionMetadata = Union[ + ApplicationCommandInteractionMetadata, + MessageComponentInteractionMetadata, + ModalInteractionMetadata, +] + + class EditApplicationCommand(TypedDict): name: str name_localizations: NotRequired[Optional[LocalizationDict]] @@ -343,8 +386,10 @@ class EditApplicationCommand(TypedDict): description_localizations: NotRequired[Optional[LocalizationDict]] options: NotRequired[Optional[List[ApplicationCommandOption]]] default_member_permissions: NotRequired[Optional[str]] - dm_permission: NotRequired[bool] + dm_permission: NotRequired[bool] # deprecated default_permission: NotRequired[bool] # deprecated nsfw: NotRequired[bool] - # TODO: remove, this cannot be changed + integration_types: NotRequired[List[ApplicationIntegrationType]] + contexts: NotRequired[List[InteractionContextType]] + # n.b. this cannot be changed type: NotRequired[ApplicationCommandType] diff --git a/disnake/types/message.py b/disnake/types/message.py index 26f691c6bb..84927b1973 100644 --- a/disnake/types/message.py +++ b/disnake/types/message.py @@ -10,7 +10,7 @@ from .components import Component from .embed import Embed from .emoji import PartialEmoji -from .interactions import InteractionMessageReference +from .interactions import InteractionMessageReference, InteractionMetadata from .member import Member, UserWithMember from .snowflake import Snowflake, SnowflakeList from .sticker import StickerItem @@ -108,7 +108,8 @@ class Message(TypedDict): message_reference: NotRequired[MessageReference] flags: NotRequired[int] referenced_message: NotRequired[Optional[Message]] - interaction: NotRequired[InteractionMessageReference] + interaction: NotRequired[InteractionMessageReference] # deprecated + interaction_metadata: NotRequired[InteractionMetadata] thread: NotRequired[Thread] components: NotRequired[List[Component]] sticker_items: NotRequired[List[StickerItem]] From 6180560bde9293b21ea139a7f034e88ffcd5b6bc Mon Sep 17 00:00:00 2001 From: shiftinv Date: Wed, 20 Mar 2024 18:38:46 +0100 Subject: [PATCH 02/36] fix(types): probably resolve todos, these fields don't exist in guild cmds --- disnake/types/interactions.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/disnake/types/interactions.py b/disnake/types/interactions.py index 2f94bf82b1..23aafdace5 100644 --- a/disnake/types/interactions.py +++ b/disnake/types/interactions.py @@ -43,10 +43,8 @@ class ApplicationCommand(TypedDict): dm_permission: NotRequired[Optional[bool]] # deprecated default_permission: NotRequired[bool] # deprecated nsfw: NotRequired[bool] - # TODO: according to the spec, this is also optional + nullable, though I haven't encountered that yet - integration_types: List[ApplicationIntegrationType] - # TODO: this appears to be optional + nullable - contexts: List[InteractionContextType] + integration_types: NotRequired[List[ApplicationIntegrationType]] + contexts: NotRequired[Optional[List[InteractionContextType]]] version: Snowflake @@ -389,7 +387,7 @@ class EditApplicationCommand(TypedDict): dm_permission: NotRequired[bool] # deprecated default_permission: NotRequired[bool] # deprecated nsfw: NotRequired[bool] - integration_types: NotRequired[List[ApplicationIntegrationType]] - contexts: NotRequired[List[InteractionContextType]] + integration_types: NotRequired[Optional[List[ApplicationIntegrationType]]] + contexts: NotRequired[Optional[List[InteractionContextType]]] # n.b. this cannot be changed type: NotRequired[ApplicationCommandType] From 8589cbb35576089a1bc4c68d74c3145abbcb84b5 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Wed, 20 Mar 2024 19:51:20 +0100 Subject: [PATCH 03/36] feat: add new fields to app command models --- disnake/app_commands.py | 209 +++++++++++++++++++++++++++++++++++--- disnake/enums.py | 14 +++ docs/api/app_commands.rst | 39 +++++++ 3 files changed, 248 insertions(+), 14 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index 727f35cb93..e4017e6123 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -5,12 +5,25 @@ import math import re from abc import ABC -from typing import TYPE_CHECKING, ClassVar, List, Mapping, Optional, Sequence, Tuple, Union +from typing import ( + TYPE_CHECKING, + ClassVar, + Collection, + List, + Mapping, + Optional, + Sequence, + Set, + Tuple, + Union, +) from .enums import ( ApplicationCommandPermissionType, ApplicationCommandType, + ApplicationIntegrationType, ChannelType, + InteractionContextType, Locale, OptionType, enum_if_int, @@ -491,19 +504,35 @@ class ApplicationCommand(ABC): .. versionadded:: 2.5 + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. .. versionadded:: 2.8 + + integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 """ __repr_info__: ClassVar[Tuple[str, ...]] = ( "type", "name", - "dm_permission", "default_member_permisions", "nsfw", + "integration_types", + "contexts", ) def __init__( @@ -513,6 +542,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, ) -> None: self.type: ApplicationCommandType = enum_if_int(ApplicationCommandType, type) @@ -521,6 +552,7 @@ def __init__( self.name_localizations: LocalizationValue = name_loc.localizations self.nsfw: bool = False if nsfw is None else nsfw + # TODO: turn this into a property based on `1 in self.contexts` instead, stop sending dm_permission self.dm_permission: bool = True if dm_permission is None else dm_permission self._default_member_permissions: Optional[int] @@ -534,6 +566,13 @@ def __init__( else: self._default_member_permissions = default_member_permissions.value + # TODO: should these be frozensets? + # TODO: we probably actually have to replicate the api default here so that sync works properly + self.integration_types: Optional[Set[ApplicationIntegrationType]] = ( + set(integration_types) if integration_types else None + ) + self.contexts: Optional[Set[InteractionContextType]] = set(contexts) if contexts else None + self._always_synced: bool = False # reset `default_permission` if set before @@ -571,13 +610,17 @@ def __eq__(self, other) -> bool: and self.name_localizations == other.name_localizations and self.nsfw == other.nsfw and self._default_member_permissions == other._default_member_permissions - # ignore `dm_permission` if comparing guild commands + # ignore global-only fields if comparing guild commands and ( any( (isinstance(obj, _APIApplicationCommandMixin) and obj.guild_id) for obj in (self, other) ) - or self.dm_permission == other.dm_permission + or ( + self.dm_permission == other.dm_permission + and self.integration_types == other.integration_types + and self.contexts == other.contexts + ) ) and self._default_permission == other._default_permission ) @@ -586,15 +629,24 @@ def to_dict(self) -> EditApplicationCommandPayload: data: EditApplicationCommandPayload = { "type": try_enum_to_int(self.type), "name": self.name, + "default_member_permissions": ( + str(self._default_member_permissions) + if self._default_member_permissions is not None + else None + ), "dm_permission": self.dm_permission, "default_permission": True, "nsfw": self.nsfw, + "integration_types": ( + list(map(try_enum_to_int, self.integration_types)) + if self.integration_types is not None + else None + ), + "contexts": ( + list(map(try_enum_to_int, self.contexts)) if self.contexts is not None else None + ), } - if self._default_member_permissions is None: - data["default_member_permissions"] = None - else: - data["default_member_permissions"] = str(self._default_member_permissions) if (loc := self.name_localizations.data) is not None: data["name_localizations"] = loc @@ -634,14 +686,29 @@ class UserCommand(ApplicationCommand): .. versionadded:: 2.5 + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. .. versionadded:: 2.8 + + integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 """ - __repr_info__ = ("name", "dm_permission", "default_member_permissions") + __repr_info__ = tuple(n for n in ApplicationCommand.__repr_info__ if n != "type") def __init__( self, @@ -649,6 +716,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, ) -> None: super().__init__( type=ApplicationCommandType.user, @@ -656,6 +725,8 @@ def __init__( dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, ) @@ -678,11 +749,26 @@ class APIUserCommand(UserCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + id: :class:`int` The user command's ID. application_id: :class:`int` @@ -706,6 +792,16 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: dm_permission=data.get("dm_permission") is not False, default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), + integration_types=( + [try_enum(ApplicationIntegrationType, t) for t in integration_types] + if (integration_types := data.get("integration_types")) is not None + else None + ), + contexts=( + [try_enum(InteractionContextType, t) for t in contexts] + if (contexts := data.get("contexts")) is not None + else None + ), ) self._update_common(data) return self @@ -729,14 +825,29 @@ class MessageCommand(ApplicationCommand): .. versionadded:: 2.5 + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. .. versionadded:: 2.8 + + integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 """ - __repr_info__ = ("name", "dm_permission", "default_member_permissions") + __repr_info__ = tuple(n for n in ApplicationCommand.__repr_info__ if n != "type") def __init__( self, @@ -744,6 +855,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, ) -> None: super().__init__( type=ApplicationCommandType.message, @@ -751,6 +864,8 @@ def __init__( dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, ) @@ -773,11 +888,26 @@ class APIMessageCommand(MessageCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + id: :class:`int` The message command's ID. application_id: :class:`int` @@ -801,6 +931,16 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: dm_permission=data.get("dm_permission") is not False, default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), + integration_types=( + [try_enum(ApplicationIntegrationType, t) for t in integration_types] + if (integration_types := data.get("integration_types")) is not None + else None + ), + contexts=( + [try_enum(InteractionContextType, t) for t in contexts] + if (contexts := data.get("contexts")) is not None + else None + ), ) self._update_common(data) return self @@ -831,22 +971,34 @@ class SlashCommand(ApplicationCommand): .. versionadded:: 2.5 + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + options: List[:class:`Option`] The list of options the slash command has. """ - __repr_info__ = ( - "name", + __repr_info__ = tuple(n for n in ApplicationCommand.__repr_info__ if n != "type") + ( "description", "options", - "dm_permission", - "default_member_permissions", ) def __init__( @@ -857,6 +1009,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, ) -> None: super().__init__( type=ApplicationCommandType.chat_input, @@ -864,6 +1018,8 @@ def __init__( dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, ) _validate_name(self.name) @@ -962,11 +1118,26 @@ class APISlashCommand(SlashCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + id: :class:`int` The slash command's ID. options: List[:class:`Option`] @@ -996,6 +1167,16 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: dm_permission=data.get("dm_permission") is not False, default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), + integration_types=( + [try_enum(ApplicationIntegrationType, t) for t in integration_types] + if (integration_types := data.get("integration_types")) is not None + else None + ), + contexts=( + [try_enum(InteractionContextType, t) for t in contexts] + if (contexts := data.get("contexts")) is not None + else None + ), ) self._update_common(data) return self diff --git a/disnake/enums.py b/disnake/enums.py index a1adceee2b..7aad9aac73 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -70,6 +70,8 @@ "OnboardingPromptType", "SKUType", "EntitlementType", + "ApplicationIntegrationType", + "InteractionContextType", ) @@ -1342,6 +1344,18 @@ class EntitlementType(Enum): application_subscription = 8 +# TODO: `guild` vs `guild_install` +class ApplicationIntegrationType(Enum): + guild = 0 + user = 1 + + +class InteractionContextType(Enum): + guild = 0 + bot_dm = 1 + private_channel = 2 + + T = TypeVar("T") diff --git a/docs/api/app_commands.rst b/docs/api/app_commands.rst index a55e3670ab..65ea356d06 100644 --- a/docs/api/app_commands.rst +++ b/docs/api/app_commands.rst @@ -194,6 +194,45 @@ ApplicationCommandPermissionType Represents a permission that affects channels. +ApplicationIntegrationType +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: ApplicationIntegrationType + + Represents the context in which an application or application command can be installed. + + See the :ddocs:`official documentation ` for more info. + + .. versionadded:: 2.10 + + .. attribute:: guild + + Installable to guilds. + .. attribute:: user + + Installable to users. + +InteractionContextType +~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: InteractionContextType + + Represents the context in which an application command can be used. + + See the :ddocs:`official documentation ` for more info. + + .. versionadded:: 2.10 + + .. attribute:: guild + + Represents a command that's usable in guilds. + .. attribute:: bot_dm + + Represents a command that's usable in DMs with the bot. + .. attribute:: private_channel + + Represents a command that's usable in DMs and group DMs with other users. + Events ------ From 83661e7928ed150ff3734a6e7c49b78411daeff8 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Wed, 20 Mar 2024 21:02:23 +0100 Subject: [PATCH 04/36] feat: add new fields in interactions --- disnake/interactions/application_command.py | 10 ++++++ disnake/interactions/base.py | 35 +++++++++++++++++++-- disnake/interactions/message.py | 10 ++++++ disnake/interactions/modal.py | 10 ++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index d25f6a2530..3880ab4ed1 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -90,6 +90,16 @@ class ApplicationCommandInteraction(Interaction[ClientT]): .. versionadded:: 2.10 + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + XXX + + .. versionadded:: 2.10 + + context: Optional[:class:`InteractionContextType`] + XXX + + .. versionadded:: 2.10 + data: :class:`ApplicationCommandInteractionData` The wrapped interaction data. application_command: :class:`.InvokableApplicationCommand` diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index ce3b5cc89b..66fcfdae7c 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -24,8 +24,10 @@ from ..channel import PartialMessageable, _threaded_guild_channel_factory from ..entitlement import Entitlement from ..enums import ( + ApplicationIntegrationType, ChannelType, ComponentType, + InteractionContextType, InteractionResponseType, InteractionType, Locale, @@ -150,6 +152,16 @@ class Interaction(Generic[ClientT]): The entitlements for the invoking user and guild, representing access to an application subscription. + .. versionadded:: 2.10 + + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + XXX + + .. versionadded:: 2.10 + + context: Optional[:class:`InteractionContextType`] + XXX + .. versionadded:: 2.10 """ @@ -167,6 +179,8 @@ class Interaction(Generic[ClientT]): "guild_locale", "client", "entitlements", + "authorizing_integration_owners", + "context", "_app_permissions", "_permissions", "_state", @@ -224,6 +238,21 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState) -> None: else [] ) + # TODO: reconsider if/how to expose this: + # type 0 is either "0" or matches self.guild.id + # type 1 should(?) match self.user.id + # ... this completely falls apart for message.interaction_metadata + self.authorizing_integration_owners: Dict[ApplicationIntegrationType, int] = { + try_enum(ApplicationIntegrationType, int(k)): int(v) + for k, v in (data.get("authorizing_integration_owners") or {}).items() + } + + self.context: Optional[InteractionContextType] = ( + try_enum(InteractionContextType, context) + if (context := data.get("context")) is not None + else None + ) + @property def bot(self) -> ClientT: """:class:`~disnake.ext.commands.Bot`: An alias for :attr:`.client`.""" @@ -251,9 +280,8 @@ def me(self) -> Union[Member, ClientUser]: """Union[:class:`.Member`, :class:`.ClientUser`]: Similar to :attr:`.Guild.me` except it may return the :class:`.ClientUser` in private message contexts. """ - if self.guild is None: - return None if self.bot is None else self.bot.user # type: ignore - return self.guild.me + # TODO: guild.me will return None once we start using the partial guild from the interaction + return self.guild.me if self.guild is not None else self.client.user @utils.cached_slot_property("_cs_channel") def channel(self) -> Union[GuildMessageable, PartialMessageable]: @@ -298,6 +326,7 @@ def app_permissions(self) -> Permissions: """ if self.guild_id: return Permissions(self._app_permissions) + # TODO: this fallback should be unnecessary now return Permissions.private_channel() @utils.cached_slot_property("_cs_response") diff --git a/disnake/interactions/message.py b/disnake/interactions/message.py index 4ef51165d5..bce2212440 100644 --- a/disnake/interactions/message.py +++ b/disnake/interactions/message.py @@ -77,6 +77,16 @@ class MessageInteraction(Interaction[ClientT]): .. versionadded:: 2.10 + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + XXX + + .. versionadded:: 2.10 + + context: Optional[:class:`InteractionContextType`] + XXX + + .. versionadded:: 2.10 + data: :class:`MessageInteractionData` The wrapped interaction data. message: Optional[:class:`Message`] diff --git a/disnake/interactions/modal.py b/disnake/interactions/modal.py index f631c38ac2..8bba0f104d 100644 --- a/disnake/interactions/modal.py +++ b/disnake/interactions/modal.py @@ -65,6 +65,16 @@ class ModalInteraction(Interaction[ClientT]): .. versionadded:: 2.10 + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + XXX + + .. versionadded:: 2.10 + + context: Optional[:class:`InteractionContextType`] + XXX + + .. versionadded:: 2.10 + data: :class:`ModalInteractionData` The wrapped interaction data. message: Optional[:class:`Message`] From ea29a3542eebd5432f483a0df04a37ca9047e98a Mon Sep 17 00:00:00 2001 From: shiftinv Date: Thu, 21 Mar 2024 16:34:39 +0100 Subject: [PATCH 05/36] docs: add interaction fields documentation that may or may not be here to stay --- disnake/interactions/application_command.py | 21 +++++++++++++++++++-- disnake/interactions/base.py | 21 +++++++++++++++++++-- disnake/interactions/message.py | 21 +++++++++++++++++++-- disnake/interactions/modal.py | 21 +++++++++++++++++++-- 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 3880ab4ed1..e3fde513aa 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -91,12 +91,29 @@ class ApplicationCommandInteraction(Interaction[ClientT]): .. versionadded:: 2.10 authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] - XXX + The authorizing user/guild for the application installation. + + This is only available if the application was installed to a user, and is empty otherwise. + If this interaction was triggered through an application command, + this requirement also applies to the command itself; see :attr:`ApplicationCommand.integration_types`. + + The value for the :attr:`ApplicationIntegrationType.user` key is the user ID. + If the application (and command) was also installed to the guild, the value for the + :attr:`ApplicationIntegrationType.guild` key is the guild ID, or ``0`` in DMs with the bot. + + See the :ddocs:`official docs ` + for more information. + + For example, this would return ``{.guild: , .user: }`` if invoked in a guild and installed to the guild and user, + or ``{.user: }`` in a DM between two users. .. versionadded:: 2.10 context: Optional[:class:`InteractionContextType`] - XXX + The context where the interaction was triggered from. + + This has the same requirements as :attr:`authorizing_integration_owners`; that is, + this is only available if the application (and command) was installed to a user, and is ``None`` otherwise. .. versionadded:: 2.10 diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index 66fcfdae7c..eefe13a580 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -155,12 +155,29 @@ class Interaction(Generic[ClientT]): .. versionadded:: 2.10 authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] - XXX + The authorizing user/guild for the application installation. + + This is only available if the application was installed to a user, and is empty otherwise. + If this interaction was triggered through an application command, + this requirement also applies to the command itself; see :attr:`ApplicationCommand.integration_types`. + + The value for the :attr:`ApplicationIntegrationType.user` key is the user ID. + If the application (and command) was also installed to the guild, the value for the + :attr:`ApplicationIntegrationType.guild` key is the guild ID, or ``0`` in DMs with the bot. + + See the :ddocs:`official docs ` + for more information. + + For example, this would return ``{.guild: , .user: }`` if invoked in a guild and installed to the guild and user, + or ``{.user: }`` in a DM between two users. .. versionadded:: 2.10 context: Optional[:class:`InteractionContextType`] - XXX + The context where the interaction was triggered from. + + This has the same requirements as :attr:`authorizing_integration_owners`; that is, + this is only available if the application (and command) was installed to a user, and is ``None`` otherwise. .. versionadded:: 2.10 """ diff --git a/disnake/interactions/message.py b/disnake/interactions/message.py index bce2212440..9b39ae71bc 100644 --- a/disnake/interactions/message.py +++ b/disnake/interactions/message.py @@ -78,12 +78,29 @@ class MessageInteraction(Interaction[ClientT]): .. versionadded:: 2.10 authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] - XXX + The authorizing user/guild for the application installation. + + This is only available if the application was installed to a user, and is empty otherwise. + If this interaction was triggered through an application command, + this requirement also applies to the command itself; see :attr:`ApplicationCommand.integration_types`. + + The value for the :attr:`ApplicationIntegrationType.user` key is the user ID. + If the application (and command) was also installed to the guild, the value for the + :attr:`ApplicationIntegrationType.guild` key is the guild ID, or ``0`` in DMs with the bot. + + See the :ddocs:`official docs ` + for more information. + + For example, this would return ``{.guild: , .user: }`` if invoked in a guild and installed to the guild and user, + or ``{.user: }`` in a DM between two users. .. versionadded:: 2.10 context: Optional[:class:`InteractionContextType`] - XXX + The context where the interaction was triggered from. + + This has the same requirements as :attr:`authorizing_integration_owners`; that is, + this is only available if the application (and command) was installed to a user, and is ``None`` otherwise. .. versionadded:: 2.10 diff --git a/disnake/interactions/modal.py b/disnake/interactions/modal.py index 8bba0f104d..dd644f7b14 100644 --- a/disnake/interactions/modal.py +++ b/disnake/interactions/modal.py @@ -66,12 +66,29 @@ class ModalInteraction(Interaction[ClientT]): .. versionadded:: 2.10 authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] - XXX + The authorizing user/guild for the application installation. + + This is only available if the application was installed to a user, and is empty otherwise. + If this interaction was triggered through an application command, + this requirement also applies to the command itself; see :attr:`ApplicationCommand.integration_types`. + + The value for the :attr:`ApplicationIntegrationType.user` key is the user ID. + If the application (and command) was also installed to the guild, the value for the + :attr:`ApplicationIntegrationType.guild` key is the guild ID, or ``0`` in DMs with the bot. + + See the :ddocs:`official docs ` + for more information. + + For example, this would return ``{.guild: , .user: }`` if invoked in a guild and installed to the guild and user, + or ``{.user: }`` in a DM between two users. .. versionadded:: 2.10 context: Optional[:class:`InteractionContextType`] - XXX + The context where the interaction was triggered from. + + This has the same requirements as :attr:`authorizing_integration_owners`; that is, + this is only available if the application (and command) was installed to a user, and is ``None`` otherwise. .. versionadded:: 2.10 From dca356e9b8d3ff971c6a4d19a6d2fe57533af56d Mon Sep 17 00:00:00 2001 From: shiftinv Date: Fri, 22 Mar 2024 14:30:24 +0100 Subject: [PATCH 06/36] refactor: deprecate `InteractionReference` --- disnake/interactions/base.py | 6 ------ disnake/message.py | 34 ++++++++++++++++++++++------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index eefe13a580..43e2b63d97 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -1553,12 +1553,6 @@ class InteractionMessage(Message): Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. reference: Optional[:class:`~disnake.MessageReference`] The message that this message references. This is only applicable to message replies. - interaction: Optional[:class:`~disnake.InteractionReference`] - The interaction that this message references. - This exists only when the message is a response to an interaction without an existing message. - - .. versionadded:: 2.1 - mention_everyone: :class:`bool` Specifies if the message mentions everyone. diff --git a/disnake/message.py b/disnake/message.py index 1bfb62cd43..610a4227f4 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -39,7 +39,7 @@ from .threads import Thread from .ui.action_row import components_to_dict from .user import User -from .utils import MISSING, assert_never, escape_mentions +from .utils import MISSING, assert_never, deprecated, escape_mentions if TYPE_CHECKING: from typing_extensions import Self @@ -679,6 +679,9 @@ class InteractionReference: .. versionadded:: 2.1 + .. deprecated:: 2.10 + Use :attr:`Message.interaction_metadata` instead. + Attributes ---------- id: :class:`int` @@ -817,12 +820,6 @@ class Message(Hashable): .. versionadded:: 1.5 - interaction: Optional[:class:`~disnake.InteractionReference`] - The interaction that this message references. - This exists only when the message is a response to an interaction without an existing message. - - .. versionadded:: 2.1 - mention_everyone: :class:`bool` Specifies if the message mentions everyone. @@ -925,7 +922,7 @@ class Message(Hashable): "flags", "reactions", "reference", - "interaction", + "_interaction", "application", "activity", "stickers", @@ -986,11 +983,11 @@ def __init__( for d in data.get("components", []) ] - inter_payload = data.get("interaction") - inter = ( - None if inter_payload is None else InteractionReference(state=state, data=inter_payload) + self._interaction: Optional[InteractionReference] = ( + InteractionReference(state=state, data=interaction) + if (interaction := data.get("interaction")) is not None + else None ) - self.interaction: Optional[InteractionReference] = inter try: # if the channel doesn't have a guild attribute, we handle that @@ -1527,6 +1524,19 @@ def system_content(self) -> Optional[str]: # in the event of an unknown or unsupported message type, we return nothing return None + @property + @deprecated("interaction_metadata") + def interaction(self) -> Optional[InteractionReference]: + """Optional[:class:`~disnake.InteractionReference`]: The interaction that this message references. + This exists only when the message is a response to an interaction without an existing message. + + .. versionadded:: 2.1 + + .. deprecated:: 2.10 + Use :attr:`interaction_metadata` instead. + """ + return self._interaction + async def delete(self, *, delay: Optional[float] = None) -> None: """|coro| From ca755ee6e077d3542dceace5c722acb90f10512c Mon Sep 17 00:00:00 2001 From: shiftinv Date: Fri, 22 Mar 2024 14:57:26 +0100 Subject: [PATCH 07/36] feat(message): add `InteractionMetadata` --- disnake/interactions/base.py | 5 ++ disnake/message.py | 97 ++++++++++++++++++++++++++++++++++-- docs/api/messages.rst | 8 +++ 3 files changed, 107 insertions(+), 3 deletions(-) diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index 43e2b63d97..d65b31efdd 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -1553,6 +1553,11 @@ class InteractionMessage(Message): Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. reference: Optional[:class:`~disnake.MessageReference`] The message that this message references. This is only applicable to message replies. + interaction_metadata: Optional[:class:`InteractionMetadata`] + The metadata about the interaction that caused this message, if any. + + .. versionadded:: 2.10 + mention_everyone: :class:`bool` Specifies if the message mentions everyone. diff --git a/disnake/message.py b/disnake/message.py index 610a4227f4..11ce269ba3 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -26,7 +26,14 @@ from .components import ActionRow, MessageComponent, _component_factory from .embeds import Embed from .emoji import Emoji -from .enums import ChannelType, InteractionType, MessageType, try_enum, try_enum_to_int +from .enums import ( + ApplicationIntegrationType, + ChannelType, + InteractionType, + MessageType, + try_enum, + try_enum_to_int, +) from .errors import HTTPException from .file import File from .flags import AttachmentFlags, MessageFlags @@ -39,7 +46,7 @@ from .threads import Thread from .ui.action_row import components_to_dict from .user import User -from .utils import MISSING, assert_never, deprecated, escape_mentions +from .utils import MISSING, _get_as_snowflake, assert_never, deprecated, escape_mentions if TYPE_CHECKING: from typing_extensions import Self @@ -60,6 +67,7 @@ ) from .types.interactions import ( InteractionMessageReference as InteractionMessageReferencePayload, + InteractionMetadata as InteractionMetadataPayload, ) from .types.member import Member as MemberPayload, UserWithMember as UserWithMemberPayload from .types.message import ( @@ -82,9 +90,10 @@ "Attachment", "Message", "PartialMessage", + "DeletedReferencedMessage", "MessageReference", "InteractionReference", - "DeletedReferencedMessage", + "InteractionMetadata", "RoleSubscriptionData", ) @@ -717,6 +726,77 @@ def author(self) -> User: return self.user +class InteractionMetadata: + """Represents metadata about the interaction that caused a particular message. + + .. versionadded:: 2.10 + + Attributes + ---------- + id: :class:`int` + The ID of the interaction. + type: :class:`InteractionType` + The type of the interaction. + user_id: :class:`int` + The ID of the user that triggered the interaction. + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + The authorizing user/guild for the application installation related to the interaction. + See :attr:`Interaction.authorizing_integration_owners` for details. + original_response_message_id: Optional[:class:`int`] + The ID of the original response message. + Only present on :attr:`~Interaction.followup` messages. + name: Optional[:class:`str`] + The name of the command, including group and subcommand name if applicable (separated by spaces). + Only present on :attr:`InteractionType.application_command` interactions. + interacted_message_id: Optional[:class:`int`] + The ID of the message containing the component. + Only present on :attr:`InteractionType.component` interactions. + triggering_interaction_metadata: Optional[InteractionMetadata] + The metadata of the original interaction that triggered the modal. + Only present on :attr:`InteractionType.modal_submit` interactions. + """ + + # TODO: do we even need state here + __slots__ = [ + "_state", + "id", + "type", + "user_id", + "authorizing_integration_owners", + "original_response_message_id", + "name", + "interacted_message_id", + "triggering_interaction_metadata", + ] + + def __init__(self, *, state: ConnectionState, data: InteractionMetadataPayload) -> None: + self._state: ConnectionState = state + + self.id: int = int(data.get("id")) + self.type: InteractionType = try_enum(InteractionType, int(data["type"])) + # TODO: consider @property def user(self) -> Member | User | None + self.user_id: int = int(data.get("user_id")) + self.authorizing_integration_owners: Dict[ApplicationIntegrationType, int] = { + try_enum(ApplicationIntegrationType, int(k)): int(v) + for k, v in (data.get("authorizing_integration_owners") or {}).items() + } + + # followup only + self.original_response_message_id: Optional[int] = _get_as_snowflake( + data, "original_response_message_id" + ) + # application command/type 2 only + self.name: Optional[str] = data.get("name") + # component/type 3 only + self.interacted_message_id: Optional[int] = _get_as_snowflake(data, "interacted_message_id") + # modal_submit/type 5 only + self.triggering_interaction_metadata: Optional[InteractionMetadata] = ( + InteractionMetadata(state=state, data=metadata) + if (metadata := data.get("triggering_interaction_metadata")) + else None + ) + + class RoleSubscriptionData: """Represents metadata of the role subscription purchase/renewal in a message of type :attr:`MessageType.role_subscription_purchase`. @@ -820,6 +900,11 @@ class Message(Hashable): .. versionadded:: 1.5 + interaction_metadata: Optional[:class:`InteractionMetadata`] + The metadata about the interaction that caused this message, if any. + + .. versionadded:: 2.10 + mention_everyone: :class:`bool` Specifies if the message mentions everyone. @@ -923,6 +1008,7 @@ class Message(Hashable): "reactions", "reference", "_interaction", + "interaction_metadata", "application", "activity", "stickers", @@ -988,6 +1074,11 @@ def __init__( if (interaction := data.get("interaction")) is not None else None ) + self.interaction_metadata: Optional[InteractionMetadata] = ( + InteractionMetadata(state=state, data=interaction) + if (interaction := data.get("interaction_metadata")) is not None + else None + ) try: # if the channel doesn't have a guild attribute, we handle that diff --git a/docs/api/messages.rst b/docs/api/messages.rst index 3031d955d9..506ea9af31 100644 --- a/docs/api/messages.rst +++ b/docs/api/messages.rst @@ -54,6 +54,14 @@ InteractionReference .. autoclass:: InteractionReference :members: +InteractionMetadata +~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionMetadata + +.. autoclass:: InteractionMetadata + :members: + RoleSubscriptionData ~~~~~~~~~~~~~~~~~~~~ From 5ab5940a9ecd19014980949fa7a71334a75ff4c4 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Fri, 22 Mar 2024 18:45:10 +0100 Subject: [PATCH 08/36] feat(appinfo): add `IntegrationTypeConfiguration` --- disnake/appinfo.py | 49 +++++++++++++++++++++++++++++++++++++++- disnake/types/appinfo.py | 6 +++-- docs/api/app_info.rst | 8 +++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/disnake/appinfo.py b/disnake/appinfo.py index 2cc80b1956..a0ca71a770 100644 --- a/disnake/appinfo.py +++ b/disnake/appinfo.py @@ -2,10 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Dict, List, Optional from . import utils from .asset import Asset +from .enums import ApplicationIntegrationType, try_enum from .flags import ApplicationFlags from .permissions import Permissions @@ -14,6 +15,7 @@ from .state import ConnectionState from .types.appinfo import ( AppInfo as AppInfoPayload, + ApplicationIntegrationTypeConfiguration as ApplicationIntegrationTypeConfigurationPayload, InstallParams as InstallParamsPayload, PartialAppInfo as PartialAppInfoPayload, Team as TeamPayload, @@ -24,6 +26,7 @@ "AppInfo", "PartialAppInfo", "InstallParams", + "IntegrationTypeConfiguration", ) @@ -65,6 +68,29 @@ def to_url(self) -> str: return utils.oauth_url(self._app_id, scopes=self.scopes, permissions=self.permissions) +class IntegrationTypeConfiguration: + """Represents the configuration for a particular application integration type. + + .. versionadded:: 2.10 + + Attributes + ---------- + install_params: Optional[:class:`InstallParams`] + The installation parameters for this integration type. + """ + + __slots__ = ("install_params",) + + def __init__( + self, data: ApplicationIntegrationTypeConfigurationPayload, parent: AppInfo + ) -> None: + self.install_params: Optional[InstallParams] = ( + InstallParams(install_params, parent=parent) + if (install_params := data.get("oauth2_install_params")) + else None + ) + + class AppInfo: """Represents the application info for the bot provided by Discord. @@ -139,6 +165,8 @@ class AppInfo: install_params: Optional[:class:`InstallParams`] The installation parameters for this application. + See also :attr:`integration_types_config` for integration type-specific configuration. + .. versionadded:: 2.5 custom_install_url: Optional[:class:`str`] @@ -151,6 +179,14 @@ class AppInfo: in the guild role verification configuration. .. versionadded:: 2.8 + integration_types_config: Dict[:class:`ApplicationIntegrationType`, :class:`IntegrationTypeConfiguration`] + The mapping of integration types/installation contexts to their respective configuration parameters. + + For example, an :attr:`ApplicationIntegrationType.guild` key being present means that + the application can be installed to guilds, and its value is the configuration (e.g. scopes, permissions) + that will be used during installation, if any. + + .. versionadded:: 2.10 """ __slots__ = ( @@ -177,6 +213,7 @@ class AppInfo: "install_params", "custom_install_url", "role_connections_verification_url", + "integration_types_config", ) def __init__(self, state: ConnectionState, data: AppInfoPayload) -> None: @@ -218,6 +255,16 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload) -> None: self.role_connections_verification_url: Optional[str] = data.get( "role_connections_verification_url" ) + # this structure is weird, but it's probably the best we're going to get. + self.integration_types_config: Dict[ + ApplicationIntegrationType, IntegrationTypeConfiguration + ] = { + try_enum(ApplicationIntegrationType, int(_type)): IntegrationTypeConfiguration( + config or {}, + parent=self, + ) + for _type, config in (data.get("integration_types_config") or {}).items() + } def __repr__(self) -> str: return ( diff --git a/disnake/types/appinfo.py b/disnake/types/appinfo.py index 785703acb5..e311851724 100644 --- a/disnake/types/appinfo.py +++ b/disnake/types/appinfo.py @@ -32,7 +32,7 @@ class InstallParams(TypedDict): permissions: str -class ApplicationIntegrationTypeConfig(TypedDict, total=False): +class ApplicationIntegrationTypeConfiguration(TypedDict, total=False): oauth2_install_params: InstallParams @@ -50,7 +50,9 @@ class AppInfo(BaseAppInfo): custom_install_url: NotRequired[str] role_connections_verification_url: NotRequired[str] # values in this dict generally shouldn't be null, but the spec claims that, so handle it just in case - integration_types_config: NotRequired[Dict[str, Optional[ApplicationIntegrationTypeConfig]]] + integration_types_config: NotRequired[ + Dict[str, Optional[ApplicationIntegrationTypeConfiguration]] + ] class PartialAppInfo(BaseAppInfo, total=False): diff --git a/docs/api/app_info.rst b/docs/api/app_info.rst index 7aa6c4148f..48dc898c13 100644 --- a/docs/api/app_info.rst +++ b/docs/api/app_info.rst @@ -34,6 +34,14 @@ InstallParams .. autoclass:: InstallParams() :members: +IntegrationTypeConfiguration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: IntegrationTypeConfiguration + +.. autoclass:: IntegrationTypeConfiguration() + :members: + Team ~~~~ From 450d279ea0fdd104240ccc5c9a24a77a094a9ee5 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Fri, 22 Mar 2024 19:14:22 +0100 Subject: [PATCH 09/36] feat: add `integration_type=` parameter to oauth urls --- disnake/appinfo.py | 36 +++++++++++++++++++++++++++--------- disnake/utils.py | 13 ++++++++++--- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/disnake/appinfo.py b/disnake/appinfo.py index a0ca71a770..a6d4f386ab 100644 --- a/disnake/appinfo.py +++ b/disnake/appinfo.py @@ -45,12 +45,20 @@ class InstallParams: __slots__ = ( "_app_id", + "_integration_type", "scopes", "permissions", ) - def __init__(self, data: InstallParamsPayload, parent: AppInfo) -> None: + def __init__( + self, + data: InstallParamsPayload, + parent: AppInfo, + *, + integration_type: Optional[ApplicationIntegrationType] = None, + ) -> None: self._app_id = parent.id + self._integration_type = integration_type self.scopes = data["scopes"] self.permissions = Permissions(int(data["permissions"])) @@ -65,7 +73,12 @@ def to_url(self) -> str: :class:`str` The invite url. """ - return utils.oauth_url(self._app_id, scopes=self.scopes, permissions=self.permissions) + return utils.oauth_url( + self._app_id, + scopes=self.scopes, + permissions=self.permissions, + integration_type=self._integration_type, + ) class IntegrationTypeConfiguration: @@ -82,10 +95,14 @@ class IntegrationTypeConfiguration: __slots__ = ("install_params",) def __init__( - self, data: ApplicationIntegrationTypeConfigurationPayload, parent: AppInfo + self, + data: ApplicationIntegrationTypeConfigurationPayload, + *, + parent: AppInfo, + integration_type: ApplicationIntegrationType, ) -> None: self.install_params: Optional[InstallParams] = ( - InstallParams(install_params, parent=parent) + InstallParams(install_params, parent=parent, integration_type=integration_type) if (install_params := data.get("oauth2_install_params")) else None ) @@ -255,16 +272,17 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload) -> None: self.role_connections_verification_url: Optional[str] = data.get( "role_connections_verification_url" ) - # this structure is weird, but it's probably the best we're going to get. + self.integration_types_config: Dict[ ApplicationIntegrationType, IntegrationTypeConfiguration - ] = { - try_enum(ApplicationIntegrationType, int(_type)): IntegrationTypeConfiguration( + ] = {} + for _type, config in (data.get("integration_types_config") or {}).items(): + integration_type = try_enum(ApplicationIntegrationType, int(_type)) + self.integration_types_config[integration_type] = IntegrationTypeConfiguration( config or {}, parent=self, + integration_type=integration_type, ) - for _type, config in (data.get("integration_types_config") or {}).items() - } def __repr__(self) -> str: return ( diff --git a/disnake/utils.py b/disnake/utils.py index 9061cd0f61..2a2ee71cfc 100644 --- a/disnake/utils.py +++ b/disnake/utils.py @@ -48,7 +48,7 @@ ) from urllib.parse import parse_qs, urlencode -from .enums import Locale +from .enums import ApplicationIntegrationType, Locale try: import orjson @@ -289,9 +289,9 @@ def oauth_url( redirect_uri: str = MISSING, scopes: Iterable[str] = MISSING, disable_guild_select: bool = False, + integration_type: Optional[ApplicationIntegrationType] = None, ) -> str: - """A helper function that returns the OAuth2 URL for inviting the bot - into guilds. + """A helper function that returns the OAuth2 URL for authorizing the application. Parameters ---------- @@ -314,6 +314,11 @@ def oauth_url( .. versionadded:: 2.0 + integration_type: Optional[:class:`~disnake.ApplicationIntegrationType`] + An optional integration type/installation context to install the application in. + + .. versionadded:: 2.10 + Returns ------- :class:`str` @@ -329,6 +334,8 @@ def oauth_url( url += "&response_type=code&" + urlencode({"redirect_uri": redirect_uri}) if disable_guild_select: url += "&disable_guild_select=true" + if integration_type is not None: + url += f"&integration_type={integration_type.value}" return url From 745b161d26d4a9106ef922291591529bfc4e5d7a Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 23 Mar 2024 14:27:27 +0100 Subject: [PATCH 10/36] fix: match api default for `integration_types` to avoid unnecessary sync --- disnake/app_commands.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index e4017e6123..ad49f17664 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -7,8 +7,10 @@ from abc import ABC from typing import ( TYPE_CHECKING, + AbstractSet, ClassVar, Collection, + Final, List, Mapping, Optional, @@ -478,6 +480,12 @@ def localize(self, store: LocalizationProtocol) -> None: o.localize(store) +# This is what the API uses by default, when not sending `integration_types` (or null). +DEFAULT_INTEGRATION_TYPES: Final[AbstractSet[ApplicationIntegrationType]] = frozenset( + {ApplicationIntegrationType.guild} +) + + class ApplicationCommand(ABC): """The base class for application commands. @@ -515,6 +523,7 @@ class ApplicationCommand(ABC): integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] The integration types/installation contexts where the command is available. + Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 @@ -566,10 +575,8 @@ def __init__( else: self._default_member_permissions = default_member_permissions.value - # TODO: should these be frozensets? - # TODO: we probably actually have to replicate the api default here so that sync works properly - self.integration_types: Optional[Set[ApplicationIntegrationType]] = ( - set(integration_types) if integration_types else None + self.integration_types: Optional[Set[ApplicationIntegrationType]] = set( + integration_types or DEFAULT_INTEGRATION_TYPES ) self.contexts: Optional[Set[InteractionContextType]] = set(contexts) if contexts else None @@ -697,6 +704,7 @@ class UserCommand(ApplicationCommand): integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] The integration types/installation contexts where the command is available. + Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 @@ -759,6 +767,7 @@ class APIUserCommand(UserCommand, _APIApplicationCommandMixin): integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] The integration types/installation contexts where the command is available. + Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 @@ -836,6 +845,7 @@ class MessageCommand(ApplicationCommand): integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] The integration types/installation contexts where the command is available. + Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 @@ -898,6 +908,7 @@ class APIMessageCommand(MessageCommand, _APIApplicationCommandMixin): integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] The integration types/installation contexts where the command is available. + Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 @@ -982,6 +993,7 @@ class SlashCommand(ApplicationCommand): integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] The integration types/installation contexts where the command is available. + Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 @@ -1128,6 +1140,7 @@ class APISlashCommand(SlashCommand, _APIApplicationCommandMixin): integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] The integration types/installation contexts where the command is available. + Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 From 545268b8a499437e01781d6ee781ca01f30fe767 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 23 Mar 2024 17:20:50 +0100 Subject: [PATCH 11/36] feat(commands): add new parameters to ext.commands --- disnake/ext/commands/base_core.py | 21 +++++++- disnake/ext/commands/ctx_menus_core.py | 45 +++++++++++++++- disnake/ext/commands/interaction_bot_base.py | 54 +++++++++++++++++++- disnake/ext/commands/slash_core.py | 24 ++++++++- 4 files changed, 140 insertions(+), 4 deletions(-) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index b5e0498399..ed20327c50 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -13,6 +13,7 @@ Dict, List, Optional, + Set, Tuple, TypeVar, Union, @@ -21,7 +22,7 @@ ) from disnake.app_commands import ApplicationCommand -from disnake.enums import ApplicationCommandType +from disnake.enums import ApplicationCommandType, ApplicationIntegrationType, InteractionContextType from disnake.permissions import Permissions from disnake.utils import _generated, _overload_with_permissions, async_all, maybe_coroutine @@ -249,6 +250,24 @@ def default_member_permissions(self) -> Optional[Permissions]: """ return self.body.default_member_permissions + @property + def integration_types(self) -> Optional[Set[ApplicationIntegrationType]]: + """Optional[Set[:class:`.ApplicationIntegrationType`]]: The integration types/installation contexts + where the command is available. Only available for global commands. + + .. versionadded:: 2.10 + """ + return self.body.integration_types + + @property + def contexts(self) -> Optional[Set[InteractionContextType]]: + """Optional[Set[:class:`.InteractionContextType`]]: The interaction contexts + where the command can be used. Only available for global commands. + + .. versionadded:: 2.10 + """ + return self.body.contexts + @property def callback(self) -> CommandCallback: return self._callback diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 835bd674e6..2ba9d4b89c 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, Collection, Dict, Optional, Sequence, Tuple, Union from disnake.app_commands import MessageCommand, UserCommand +from disnake.enums import ApplicationIntegrationType, InteractionContextType from disnake.i18n import Localized from disnake.permissions import Permissions @@ -76,6 +77,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, **kwargs, @@ -103,6 +106,8 @@ def __init__( dm_permission=dm_permission and not self._guild_only, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, ) async def _call_external_error_handlers( @@ -178,6 +183,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, **kwargs, @@ -199,6 +206,8 @@ def __init__( dm_permission=dm_permission and not self._guild_only, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, ) async def _call_external_error_handlers( @@ -234,6 +243,8 @@ def user_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -264,6 +275,19 @@ def user_command( .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`.InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. guild_ids: Sequence[:class:`int`] @@ -298,6 +322,8 @@ def decorator( dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, guild_ids=guild_ids, auto_sync=auto_sync, extras=extras, @@ -313,6 +339,8 @@ def message_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -346,6 +374,19 @@ def message_command( .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`.InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. guild_ids: Sequence[:class:`int`] @@ -380,6 +421,8 @@ def decorator( dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, guild_ids=guild_ids, auto_sync=auto_sync, extras=extras, diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 5349abeb3e..527d6728cb 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -12,6 +12,7 @@ TYPE_CHECKING, Any, Callable, + Collection, Dict, Iterable, List, @@ -27,7 +28,7 @@ import disnake from disnake.app_commands import ApplicationCommand, Option from disnake.custom_warnings import SyncWarning -from disnake.enums import ApplicationCommandType +from disnake.enums import ApplicationCommandType, ApplicationIntegrationType, InteractionContextType from disnake.utils import warn_deprecated from . import errors @@ -489,6 +490,8 @@ def slash_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, options: Optional[List[Option]] = None, guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, @@ -531,6 +534,19 @@ def slash_command( .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`.InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True`` guild_ids: Sequence[:class:`int`] @@ -564,6 +580,8 @@ def decorator(func: CommandCallback) -> InvokableSlashCommand: dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, guild_ids=guild_ids, connectors=connectors, auto_sync=auto_sync, @@ -582,6 +600,8 @@ def user_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -615,6 +635,19 @@ def user_command( .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`.InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True``. guild_ids: Sequence[:class:`int`] @@ -642,6 +675,8 @@ def decorator( dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, guild_ids=guild_ids, auto_sync=auto_sync, extras=extras, @@ -659,6 +694,8 @@ def message_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -692,6 +729,19 @@ def message_command( .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`.InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + auto_sync: :class:`bool` Whether to automatically register the command. Defaults to ``True`` guild_ids: Sequence[:class:`int`] @@ -719,6 +769,8 @@ def decorator( dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, guild_ids=guild_ids, auto_sync=auto_sync, extras=extras, diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index a23cf86bd3..d62474974a 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -8,6 +8,7 @@ TYPE_CHECKING, Any, Callable, + Collection, Dict, List, Optional, @@ -19,7 +20,7 @@ from disnake import utils from disnake.app_commands import Option, SlashCommand -from disnake.enums import OptionType +from disnake.enums import ApplicationIntegrationType, InteractionContextType, OptionType from disnake.i18n import Localized from disnake.interactions import ApplicationCommandInteraction from disnake.permissions import Permissions @@ -436,6 +437,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, auto_sync: Optional[bool] = None, @@ -472,6 +475,8 @@ def __init__( dm_permission=dm_permission and not self._guild_only, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, ) @property @@ -753,6 +758,8 @@ def slash_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, + integration_types: Optional[Collection[ApplicationIntegrationType]] = None, + contexts: Optional[Collection[InteractionContextType]] = None, options: Optional[List[Option]] = None, guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, @@ -784,6 +791,19 @@ def slash_command( .. versionadded:: 2.8 + integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + The integration types/installation contexts where the command is available. + Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Only available for global commands. + + .. versionadded:: 2.10 + + contexts: Optional[Set[:class:`.InteractionContextType`]] + The interaction contexts where the command can be used. + Only available for global commands. + + .. versionadded:: 2.10 + options: List[:class:`.Option`] The list of slash command options. The options will be visible in Discord. This is the old way of specifying options. Consider using :ref:`param_syntax` instead. @@ -834,6 +854,8 @@ def decorator(func: CommandCallback) -> InvokableSlashCommand: dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, + integration_types=integration_types, + contexts=contexts, guild_ids=guild_ids, connectors=connectors, auto_sync=auto_sync, From a3736c279644d58753f0853f8d9cb7be81fa96a8 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 23 Mar 2024 18:20:21 +0100 Subject: [PATCH 12/36] docs: add changelog entry --- changelog/1173.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/1173.feature.rst diff --git a/changelog/1173.feature.rst b/changelog/1173.feature.rst new file mode 100644 index 0000000000..a0990367ef --- /dev/null +++ b/changelog/1173.feature.rst @@ -0,0 +1 @@ +TBD From a33c95d7c80921bca5e1fd281f69cf11d4335200 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 23 Mar 2024 19:09:44 +0100 Subject: [PATCH 13/36] fix(types): update `interaction_metadata.name` type --- disnake/types/interactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/types/interactions.py b/disnake/types/interactions.py index 23aafdace5..61611f1237 100644 --- a/disnake/types/interactions.py +++ b/disnake/types/interactions.py @@ -356,7 +356,7 @@ class _BaseInteractionMetadata(TypedDict): class ApplicationCommandInteractionMetadata(_BaseInteractionMetadata): - name: NotRequired[str] # not documented + name: str # not documented class MessageComponentInteractionMetadata(_BaseInteractionMetadata): From c06f9c31fc1bb15dd571a6d1e356965623383e2f Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 19 Aug 2024 19:56:04 +0200 Subject: [PATCH 14/36] fix: add `InteractionMetadata.user` --- disnake/message.py | 10 +++++----- disnake/types/interactions.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/disnake/message.py b/disnake/message.py index faf6395f2c..8d7192ca27 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -737,8 +737,8 @@ class InteractionMetadata: The ID of the interaction. type: :class:`InteractionType` The type of the interaction. - user_id: :class:`int` - The ID of the user that triggered the interaction. + user: :class:`User` + The user that triggered the interaction. authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] The authorizing user/guild for the application installation related to the interaction. See :attr:`Interaction.authorizing_integration_owners` for details. @@ -761,7 +761,7 @@ class InteractionMetadata: "_state", "id", "type", - "user_id", + "user", "authorizing_integration_owners", "original_response_message_id", "name", @@ -774,8 +774,8 @@ def __init__(self, *, state: ConnectionState, data: InteractionMetadataPayload) self.id: int = int(data.get("id")) self.type: InteractionType = try_enum(InteractionType, int(data["type"])) - # TODO: consider @property def user(self) -> Member | User | None - self.user_id: int = int(data.get("user_id")) + # TODO: consider trying member cache first? + self.user: User = state.store_user(data["user"]) self.authorizing_integration_owners: Dict[ApplicationIntegrationType, int] = { try_enum(ApplicationIntegrationType, int(k)): int(v) for k, v in (data.get("authorizing_integration_owners") or {}).items() diff --git a/disnake/types/interactions.py b/disnake/types/interactions.py index 61611f1237..da696cb13b 100644 --- a/disnake/types/interactions.py +++ b/disnake/types/interactions.py @@ -349,13 +349,14 @@ class InteractionMessageReference(TypedDict): class _BaseInteractionMetadata(TypedDict): id: Snowflake type: InteractionType - user_id: Snowflake + user: User # keys are stringified ApplicationIntegrationType's authorizing_integration_owners: Dict[str, Snowflake] original_response_message_id: NotRequired[Snowflake] # only on followups class ApplicationCommandInteractionMetadata(_BaseInteractionMetadata): + # TODO: consider removing this again name: str # not documented From 10a8bbbafb005d4979b08d7dfa2a508bc4d6f057 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 20 Aug 2024 12:14:17 +0200 Subject: [PATCH 15/36] chore: remove weird `int(dict.get())` call --- disnake/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/message.py b/disnake/message.py index 8d7192ca27..7fdecb938a 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -772,7 +772,7 @@ class InteractionMetadata: def __init__(self, *, state: ConnectionState, data: InteractionMetadataPayload) -> None: self._state: ConnectionState = state - self.id: int = int(data.get("id")) + self.id: int = int(data["id"]) self.type: InteractionType = try_enum(InteractionType, int(data["type"])) # TODO: consider trying member cache first? self.user: User = state.store_user(data["user"]) From 93de3c8726a363095a881fb8f728fc4cd55c7494 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 23 Sep 2024 06:01:09 -0400 Subject: [PATCH 16/36] fix: make dm_permission a property --- disnake/app_commands.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index ad49f17664..26738f9fb8 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -561,8 +561,7 @@ def __init__( self.name_localizations: LocalizationValue = name_loc.localizations self.nsfw: bool = False if nsfw is None else nsfw - # TODO: turn this into a property based on `1 in self.contexts` instead, stop sending dm_permission - self.dm_permission: bool = True if dm_permission is None else dm_permission + self._dm_permission: Optional[bool] = dm_permission self._default_member_permissions: Optional[int] if default_member_permissions is None: @@ -603,6 +602,19 @@ def default_member_permissions(self) -> Optional[Permissions]: return None return Permissions(self._default_member_permissions) + @property + def dm_permission(self): + # check if contexts are in use at all + if self.contexts: + return InteractionContextType.bot_dm in self.contexts + + return self._dm_permission + + @dm_permission.setter + def dm_permission(self, value: bool) -> None: + # todo: add a deprecation warning here + self._dm_permission = value + def __repr__(self) -> str: attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__) return f"<{type(self).__name__} {attrs}>" @@ -641,7 +653,6 @@ def to_dict(self) -> EditApplicationCommandPayload: if self._default_member_permissions is not None else None ), - "dm_permission": self.dm_permission, "default_permission": True, "nsfw": self.nsfw, "integration_types": ( @@ -657,6 +668,9 @@ def to_dict(self) -> EditApplicationCommandPayload: if (loc := self.name_localizations.data) is not None: data["name_localizations"] = loc + if not self.contexts and self.dm_permission is not None: + data["dm_permission"] = self.dm_permission + return data def localize(self, store: LocalizationProtocol) -> None: From fa87500ed82fd53fb30aeaccf7dedaecb32f156b Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 16 Sep 2024 18:56:59 +0200 Subject: [PATCH 17/36] refactor: turn `ApplicationIntegrationType` and `InteractionContextType` into flags instead of enums --- disnake/app_commands.py | 105 +++++------ disnake/appinfo.py | 17 +- disnake/enums.py | 14 -- disnake/ext/commands/base_core.py | 12 +- disnake/ext/commands/ctx_menus_core.py | 28 +-- disnake/ext/commands/interaction_bot_base.py | 28 +-- disnake/ext/commands/slash_core.py | 16 +- disnake/flags.py | 184 +++++++++++++++++++ disnake/interactions/base.py | 12 +- disnake/message.py | 7 +- disnake/utils.py | 9 +- docs/api/app_commands.rst | 55 ++---- 12 files changed, 306 insertions(+), 181 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index 26738f9fb8..041b2d8019 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -5,33 +5,19 @@ import math import re from abc import ABC -from typing import ( - TYPE_CHECKING, - AbstractSet, - ClassVar, - Collection, - Final, - List, - Mapping, - Optional, - Sequence, - Set, - Tuple, - Union, -) +from typing import TYPE_CHECKING, ClassVar, List, Mapping, Optional, Sequence, Tuple, Union from .enums import ( ApplicationCommandPermissionType, ApplicationCommandType, - ApplicationIntegrationType, ChannelType, - InteractionContextType, Locale, OptionType, enum_if_int, try_enum, try_enum_to_int, ) +from .flags import ApplicationIntegrationType, InteractionContextType from .i18n import Localized from .permissions import Permissions from .utils import MISSING, _get_as_snowflake, _maybe_cast @@ -480,12 +466,6 @@ def localize(self, store: LocalizationProtocol) -> None: o.localize(store) -# This is what the API uses by default, when not sending `integration_types` (or null). -DEFAULT_INTEGRATION_TYPES: Final[AbstractSet[ApplicationIntegrationType]] = frozenset( - {ApplicationIntegrationType.guild} -) - - class ApplicationCommand(ABC): """The base class for application commands. @@ -521,14 +501,14 @@ class ApplicationCommand(ABC): .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + integration_types: Optional[:class:`ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`InteractionContextType`]] + contexts: Optional[:class:`InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -551,8 +531,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, ) -> None: self.type: ApplicationCommandType = enum_if_int(ApplicationCommandType, type) @@ -574,10 +554,11 @@ def __init__( else: self._default_member_permissions = default_member_permissions.value - self.integration_types: Optional[Set[ApplicationIntegrationType]] = set( - integration_types or DEFAULT_INTEGRATION_TYPES - ) - self.contexts: Optional[Set[InteractionContextType]] = set(contexts) if contexts else None + # XXX: is this actually optional? reconsider the default/fallback value here. + self.integration_types: Optional[ + ApplicationIntegrationType + ] = integration_types or ApplicationIntegrationType(guild=True) + self.contexts: Optional[InteractionContextType] = contexts self._always_synced: bool = False @@ -655,16 +636,16 @@ def to_dict(self) -> EditApplicationCommandPayload: ), "default_permission": True, "nsfw": self.nsfw, - "integration_types": ( - list(map(try_enum_to_int, self.integration_types)) - if self.integration_types is not None - else None - ), - "contexts": ( - list(map(try_enum_to_int, self.contexts)) if self.contexts is not None else None - ), } + integration_types = ( + self.integration_types.values if self.integration_types is not None else None + ) + data["integration_types"] = integration_types + + contexts = self.contexts.values if self.contexts is not None else None + data["contexts"] = contexts + if (loc := self.name_localizations.data) is not None: data["name_localizations"] = loc @@ -716,14 +697,14 @@ class UserCommand(ApplicationCommand): .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + integration_types: Optional[:class:`ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`InteractionContextType`]] + contexts: Optional[:class:`InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -738,8 +719,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, ) -> None: super().__init__( type=ApplicationCommandType.user, @@ -779,14 +760,14 @@ class APIUserCommand(UserCommand, _APIApplicationCommandMixin): .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + integration_types: Optional[:class:`ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`InteractionContextType`]] + contexts: Optional[:class:`InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -816,12 +797,12 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), integration_types=( - [try_enum(ApplicationIntegrationType, t) for t in integration_types] + ApplicationIntegrationType._from_values(integration_types) if (integration_types := data.get("integration_types")) is not None else None ), contexts=( - [try_enum(InteractionContextType, t) for t in contexts] + InteractionContextType._from_values(contexts) if (contexts := data.get("contexts")) is not None else None ), @@ -857,14 +838,14 @@ class MessageCommand(ApplicationCommand): .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + integration_types: Optional[:class:`ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`InteractionContextType`]] + contexts: Optional[:class:`InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -879,8 +860,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, ) -> None: super().__init__( type=ApplicationCommandType.message, @@ -920,14 +901,14 @@ class APIMessageCommand(MessageCommand, _APIApplicationCommandMixin): .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + integration_types: Optional[:class:`ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`InteractionContextType`]] + contexts: Optional[:class:`InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -957,12 +938,12 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), integration_types=( - [try_enum(ApplicationIntegrationType, t) for t in integration_types] + ApplicationIntegrationType._from_values(integration_types) if (integration_types := data.get("integration_types")) is not None else None ), contexts=( - [try_enum(InteractionContextType, t) for t in contexts] + InteractionContextType._from_values(contexts) if (contexts := data.get("contexts")) is not None else None ), @@ -1005,14 +986,14 @@ class SlashCommand(ApplicationCommand): .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + integration_types: Optional[:class:`ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`InteractionContextType`]] + contexts: Optional[:class:`InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -1035,8 +1016,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, ) -> None: super().__init__( type=ApplicationCommandType.chat_input, @@ -1152,14 +1133,14 @@ class APISlashCommand(SlashCommand, _APIApplicationCommandMixin): .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`ApplicationIntegrationType`]] + integration_types: Optional[:class:`ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`InteractionContextType`]] + contexts: Optional[:class:`InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -1195,12 +1176,12 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), integration_types=( - [try_enum(ApplicationIntegrationType, t) for t in integration_types] + ApplicationIntegrationType._from_values(integration_types) if (integration_types := data.get("integration_types")) is not None else None ), contexts=( - [try_enum(InteractionContextType, t) for t in contexts] + InteractionContextType._from_values(contexts) if (contexts := data.get("contexts")) is not None else None ), diff --git a/disnake/appinfo.py b/disnake/appinfo.py index a99dbfea02..058042125b 100644 --- a/disnake/appinfo.py +++ b/disnake/appinfo.py @@ -2,11 +2,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Dict, List, Optional, cast from . import utils from .asset import Asset -from .enums import ApplicationIntegrationType, try_enum from .flags import ApplicationFlags from .permissions import Permissions @@ -15,6 +14,7 @@ from .state import ConnectionState from .types.appinfo import ( AppInfo as AppInfoPayload, + ApplicationIntegrationType as ApplicationIntegrationTypeLiteral, ApplicationIntegrationTypeConfiguration as ApplicationIntegrationTypeConfigurationPayload, InstallParams as InstallParamsPayload, PartialAppInfo as PartialAppInfoPayload, @@ -55,10 +55,10 @@ def __init__( data: InstallParamsPayload, parent: AppInfo, *, - integration_type: Optional[ApplicationIntegrationType] = None, + integration_type: Optional[ApplicationIntegrationTypeLiteral] = None, ) -> None: self._app_id = parent.id - self._integration_type = integration_type + self._integration_type: Optional[ApplicationIntegrationTypeLiteral] = integration_type self.scopes = data["scopes"] self.permissions = Permissions(int(data["permissions"])) @@ -99,7 +99,7 @@ def __init__( data: ApplicationIntegrationTypeConfigurationPayload, *, parent: AppInfo, - integration_type: ApplicationIntegrationType, + integration_type: ApplicationIntegrationTypeLiteral, ) -> None: self.install_params: Optional[InstallParams] = ( InstallParams(install_params, parent=parent, integration_type=integration_type) @@ -285,11 +285,10 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload) -> None: self.approximate_guild_count: int = data.get("approximate_guild_count", 0) self.approximate_user_install_count: int = data.get("approximate_user_install_count", 0) - self.integration_types_config: Dict[ - ApplicationIntegrationType, IntegrationTypeConfiguration - ] = {} + # TODO: update docs + self.integration_types_config: Dict[int, IntegrationTypeConfiguration] = {} for _type, config in (data.get("integration_types_config") or {}).items(): - integration_type = try_enum(ApplicationIntegrationType, int(_type)) + integration_type = cast("ApplicationIntegrationTypeLiteral", int(_type)) self.integration_types_config[integration_type] = IntegrationTypeConfiguration( config or {}, parent=self, diff --git a/disnake/enums.py b/disnake/enums.py index 566d34fa36..6f81211156 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -72,8 +72,6 @@ "SKUType", "EntitlementType", "PollLayoutType", - "ApplicationIntegrationType", - "InteractionContextType", ) @@ -1387,18 +1385,6 @@ class PollLayoutType(Enum): default = 1 -# TODO: `guild` vs `guild_install` -class ApplicationIntegrationType(Enum): - guild = 0 - user = 1 - - -class InteractionContextType(Enum): - guild = 0 - bot_dm = 1 - private_channel = 2 - - T = TypeVar("T") diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index af9583fcfe..2ae2409040 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -13,7 +13,6 @@ Dict, List, Optional, - Set, Tuple, TypeVar, Union, @@ -22,7 +21,8 @@ ) from disnake.app_commands import ApplicationCommand -from disnake.enums import ApplicationCommandType, ApplicationIntegrationType, InteractionContextType +from disnake.enums import ApplicationCommandType +from disnake.flags import ApplicationIntegrationType, InteractionContextType from disnake.permissions import Permissions from disnake.utils import _generated, _overload_with_permissions, async_all, maybe_coroutine @@ -251,8 +251,8 @@ def default_member_permissions(self) -> Optional[Permissions]: return self.body.default_member_permissions @property - def integration_types(self) -> Optional[Set[ApplicationIntegrationType]]: - """Optional[Set[:class:`.ApplicationIntegrationType`]]: The integration types/installation contexts + def integration_types(self) -> Optional[ApplicationIntegrationType]: + """Optional[:class:`.ApplicationIntegrationType`]: The integration types/installation contexts where the command is available. Only available for global commands. .. versionadded:: 2.10 @@ -260,8 +260,8 @@ def integration_types(self) -> Optional[Set[ApplicationIntegrationType]]: return self.body.integration_types @property - def contexts(self) -> Optional[Set[InteractionContextType]]: - """Optional[Set[:class:`.InteractionContextType`]]: The interaction contexts + def contexts(self) -> Optional[InteractionContextType]: + """Optional[:class:`.InteractionContextType`]: The interaction contexts where the command can be used. Only available for global commands. .. versionadded:: 2.10 diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 02e7b26f55..86023d7541 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -3,10 +3,10 @@ from __future__ import annotations import asyncio -from typing import TYPE_CHECKING, Any, Callable, Collection, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Tuple, Union from disnake.app_commands import MessageCommand, UserCommand -from disnake.enums import ApplicationIntegrationType, InteractionContextType +from disnake.flags import ApplicationIntegrationType, InteractionContextType from disnake.i18n import Localized from disnake.permissions import Permissions @@ -77,8 +77,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, **kwargs: Any, @@ -187,8 +187,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, **kwargs: Any, @@ -251,8 +251,8 @@ def user_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -283,14 +283,14 @@ def user_command( .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + integration_types: Optional[:class:`.ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`.ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`.InteractionContextType`]] + contexts: Optional[:class:`.InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -347,8 +347,8 @@ def message_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -382,14 +382,14 @@ def message_command( .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + integration_types: Optional[:class:`.ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`.ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`.InteractionContextType`]] + contexts: Optional[:class:`.InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 49afde0536..6a0a407f9c 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -12,7 +12,6 @@ TYPE_CHECKING, Any, Callable, - Collection, Dict, Iterable, List, @@ -28,7 +27,8 @@ import disnake from disnake.app_commands import ApplicationCommand, Option from disnake.custom_warnings import SyncWarning -from disnake.enums import ApplicationCommandType, ApplicationIntegrationType, InteractionContextType +from disnake.enums import ApplicationCommandType +from disnake.flags import ApplicationIntegrationType, InteractionContextType from disnake.utils import warn_deprecated from . import errors @@ -490,8 +490,8 @@ def slash_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, options: Optional[List[Option]] = None, guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, @@ -534,14 +534,14 @@ def slash_command( .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + integration_types: Optional[:class:`.ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`.ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`.InteractionContextType`]] + contexts: Optional[:class:`.InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -600,8 +600,8 @@ def user_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -635,14 +635,14 @@ def user_command( .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + integration_types: Optional[:class:`.ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`.ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`.InteractionContextType`]] + contexts: Optional[:class:`.InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. @@ -694,8 +694,8 @@ def message_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -729,14 +729,14 @@ def message_command( .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + integration_types: Optional[:class:`.ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`.ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`.InteractionContextType`]] + contexts: Optional[:class:`.InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index 01b4165c10..4582aa7dec 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -8,7 +8,6 @@ TYPE_CHECKING, Any, Callable, - Collection, Dict, List, Optional, @@ -20,7 +19,8 @@ from disnake import utils from disnake.app_commands import Option, SlashCommand -from disnake.enums import ApplicationIntegrationType, InteractionContextType, OptionType +from disnake.enums import OptionType +from disnake.flags import ApplicationIntegrationType, InteractionContextType from disnake.i18n import Localized from disnake.interactions import ApplicationCommandInteraction from disnake.permissions import Permissions @@ -437,8 +437,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, auto_sync: Optional[bool] = None, @@ -758,8 +758,8 @@ def slash_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[Collection[ApplicationIntegrationType]] = None, - contexts: Optional[Collection[InteractionContextType]] = None, + integration_types: Optional[ApplicationIntegrationType] = None, + contexts: Optional[InteractionContextType] = None, options: Optional[List[Option]] = None, guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, @@ -791,14 +791,14 @@ def slash_command( .. versionadded:: 2.8 - integration_types: Optional[Set[:class:`.ApplicationIntegrationType`]] + integration_types: Optional[:class:`.ApplicationIntegrationType`] The integration types/installation contexts where the command is available. Defaults to :attr:`.ApplicationIntegrationType.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[Set[:class:`.InteractionContextType`]] + contexts: Optional[:class:`.InteractionContextType`] The interaction contexts where the command can be used. Only available for global commands. diff --git a/disnake/flags.py b/disnake/flags.py index 406095a6d2..d8a9171549 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -43,6 +43,8 @@ "RoleFlags", "AttachmentFlags", "SKUFlags", + "ApplicationIntegrationType", + "InteractionContextType", ) BF = TypeVar("BF", bound="BaseFlags") @@ -2623,3 +2625,185 @@ def guild_subscription(self): def user_subscription(self): """:class:`bool`: Returns ``True`` if the SKU is an application subscription applied to a user.""" return 1 << 8 + + +class ApplicationIntegrationType(ListBaseFlags): + """Represents the location(s) in which an application or application command can be installed. + + See the :ddocs:`official documentation ` for more info. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two ApplicationIntegrationType instances are equal. + .. describe:: x != y + + Checks if two ApplicationIntegrationType instances are not equal. + .. describe:: x <= y + + Checks if an ApplicationIntegrationType instance is a subset of another ApplicationIntegrationType instance. + .. describe:: x >= y + + Checks if an ApplicationIntegrationType instance is a superset of another ApplicationIntegrationType instance. + .. describe:: x < y + + Checks if an ApplicationIntegrationType instance is a strict subset of another ApplicationIntegrationType instance. + .. describe:: x > y + + Checks if an ApplicationIntegrationType instance is a strict superset of another ApplicationIntegrationType instance. + .. describe:: x | y, x |= y + + Returns a new ApplicationIntegrationType instance with all enabled flags from both x and y. + (Using ``|=`` will update in place). + .. describe:: x & y, x &= y + + Returns a new ApplicationIntegrationType instance with only flags enabled on both x and y. + (Using ``&=`` will update in place). + .. describe:: x ^ y, x ^= y + + Returns a new ApplicationIntegrationType instance with only flags enabled on one of x or y, but not both. + (Using ``^=`` will update in place). + .. describe:: ~x + + Returns a new ApplicationIntegrationType instance with all flags from x inverted. + .. describe:: hash(x) + + Returns 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:: ApplicationIntegrationType.y | ApplicationIntegrationType.z, ApplicationIntegrationType(y=True) | ApplicationIntegrationType.z + + Returns a ApplicationIntegrationType instance with all provided flags enabled. + + .. describe:: ~ApplicationIntegrationType.y + + Returns a ApplicationIntegrationType instance with all flags except ``y`` inverted from their default value. + + .. versionadded:: 2.10 + + Attributes + ---------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + __slots__ = () + + if TYPE_CHECKING: + + @_generated + def __init__(self, *, guild: bool = ..., user: bool = ...) -> None: + ... + + # TODO: `guild` vs `guild_install` + @flag_value + def guild(self): + """:class:`bool`: Returns ``True`` if installable to guilds.""" + return 1 << 0 + + @flag_value + def user(self): + """:class:`bool`: Returns ``True`` if installable to users.""" + return 1 << 1 + + +class InteractionContextType(ListBaseFlags): + """Represents the context in which an application command can be used. + + See the :ddocs:`official documentation ` for more info. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two InteractionContextType instances are equal. + .. describe:: x != y + + Checks if two InteractionContextType instances are not equal. + .. describe:: x <= y + + Checks if an InteractionContextType instance is a subset of another InteractionContextType instance. + .. describe:: x >= y + + Checks if an InteractionContextType instance is a superset of another InteractionContextType instance. + .. describe:: x < y + + Checks if an InteractionContextType instance is a strict subset of another InteractionContextType instance. + .. describe:: x > y + + Checks if an InteractionContextType instance is a strict superset of another InteractionContextType instance. + .. describe:: x | y, x |= y + + Returns a new InteractionContextType instance with all enabled flags from both x and y. + (Using ``|=`` will update in place). + .. describe:: x & y, x &= y + + Returns a new InteractionContextType instance with only flags enabled on both x and y. + (Using ``&=`` will update in place). + .. describe:: x ^ y, x ^= y + + Returns a new InteractionContextType instance with only flags enabled on one of x or y, but not both. + (Using ``^=`` will update in place). + .. describe:: ~x + + Returns a new InteractionContextType instance with all flags from x inverted. + .. describe:: hash(x) + + Returns 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:: InteractionContextType.y | InteractionContextType.z, InteractionContextType(y=True) | InteractionContextType.z + + Returns a InteractionContextType instance with all provided flags enabled. + + .. describe:: ~InteractionContextType.y + + Returns a InteractionContextType instance with all flags except ``y`` inverted from their default value. + + .. versionadded:: 2.10 + + Attributes + ---------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + __slots__ = () + + if TYPE_CHECKING: + + @_generated + def __init__( + self, *, bot_dm: bool = ..., guild: bool = ..., private_channel: bool = ... + ) -> None: + ... + + @flag_value + def guild(self): + """:class:`bool`: Returns ``True`` if the command is usable in guilds.""" + return 1 << 0 + + @flag_value + def bot_dm(self): + """:class:`bool`: Returns ``True`` if the command is usable in DMs with the bot.""" + return 1 << 1 + + @flag_value + def private_channel(self): + """:class:`bool`: Returns ``True`` if the command is usable in DMs and group DMs with other users.""" + return 1 << 2 diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index 2e4742a19e..4e1a5fc575 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -24,9 +24,7 @@ from ..channel import PartialMessageable from ..entitlement import Entitlement from ..enums import ( - ApplicationIntegrationType, ComponentType, - InteractionContextType, InteractionResponseType, InteractionType, Locale, @@ -43,7 +41,7 @@ ModalChainNotSupported, NotFound, ) -from ..flags import MessageFlags +from ..flags import InteractionContextType, MessageFlags from ..guild import Guild from ..i18n import Localized from ..member import Member @@ -279,13 +277,13 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState) -> None: # type 0 is either "0" or matches self.guild.id # type 1 should(?) match self.user.id # ... this completely falls apart for message.interaction_metadata - self.authorizing_integration_owners: Dict[ApplicationIntegrationType, int] = { - try_enum(ApplicationIntegrationType, int(k)): int(v) - for k, v in (data.get("authorizing_integration_owners") or {}).items() + self.authorizing_integration_owners: Dict[int, int] = { + int(k): int(v) for k, v in (data.get("authorizing_integration_owners") or {}).items() } + # TODO: document this properly; it's a flag object, but only one value will be set self.context: Optional[InteractionContextType] = ( - try_enum(InteractionContextType, context) + InteractionContextType._from_values([context]) if (context := data.get("context")) is not None else None ) diff --git a/disnake/message.py b/disnake/message.py index fb113394e2..10ece00332 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -27,7 +27,6 @@ from .embeds import Embed from .emoji import Emoji from .enums import ( - ApplicationIntegrationType, ChannelType, InteractionType, MessageType, @@ -811,9 +810,9 @@ def __init__(self, *, state: ConnectionState, data: InteractionMetadataPayload) self.type: InteractionType = try_enum(InteractionType, int(data["type"])) # TODO: consider trying member cache first? self.user: User = state.store_user(data["user"]) - self.authorizing_integration_owners: Dict[ApplicationIntegrationType, int] = { - try_enum(ApplicationIntegrationType, int(k)): int(v) - for k, v in (data.get("authorizing_integration_owners") or {}).items() + # TODO: update docs + self.authorizing_integration_owners: Dict[int, int] = { + int(k): int(v) for k, v in (data.get("authorizing_integration_owners") or {}).items() } # followup only diff --git a/disnake/utils.py b/disnake/utils.py index 2a2ee71cfc..419d075b27 100644 --- a/disnake/utils.py +++ b/disnake/utils.py @@ -48,7 +48,7 @@ ) from urllib.parse import parse_qs, urlencode -from .enums import ApplicationIntegrationType, Locale +from .enums import Locale try: import orjson @@ -120,6 +120,7 @@ def __get__(self, instance, owner): from .invite import Invite from .permissions import Permissions from .template import Template + from .types.appinfo import ApplicationIntegrationType as ApplicationIntegrationTypeLiteral class _RequestLike(Protocol): headers: Mapping[str, Any] @@ -289,7 +290,7 @@ def oauth_url( redirect_uri: str = MISSING, scopes: Iterable[str] = MISSING, disable_guild_select: bool = False, - integration_type: Optional[ApplicationIntegrationType] = None, + integration_type: Optional[ApplicationIntegrationTypeLiteral] = None, ) -> str: """A helper function that returns the OAuth2 URL for authorizing the application. @@ -314,7 +315,7 @@ def oauth_url( .. versionadded:: 2.0 - integration_type: Optional[:class:`~disnake.ApplicationIntegrationType`] + integration_type: Optional[:class:`int`] An optional integration type/installation context to install the application in. .. versionadded:: 2.10 @@ -335,7 +336,7 @@ def oauth_url( if disable_guild_select: url += "&disable_guild_select=true" if integration_type is not None: - url += f"&integration_type={integration_type.value}" + url += f"&integration_type={integration_type}" return url diff --git a/docs/api/app_commands.rst b/docs/api/app_commands.rst index 65ea356d06..2aa9e31bbc 100644 --- a/docs/api/app_commands.rst +++ b/docs/api/app_commands.rst @@ -108,6 +108,22 @@ OptionChoice .. autoclass:: OptionChoice() :members: +ApplicationIntegrationType +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: ApplicationIntegrationType + +.. autoclass:: ApplicationIntegrationType() + :members: + +InteractionContextType +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionContextType + +.. autoclass:: InteractionContextType() + :members: + Enumerations ------------ @@ -194,45 +210,6 @@ ApplicationCommandPermissionType Represents a permission that affects channels. -ApplicationIntegrationType -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. class:: ApplicationIntegrationType - - Represents the context in which an application or application command can be installed. - - See the :ddocs:`official documentation ` for more info. - - .. versionadded:: 2.10 - - .. attribute:: guild - - Installable to guilds. - .. attribute:: user - - Installable to users. - -InteractionContextType -~~~~~~~~~~~~~~~~~~~~~~ - -.. class:: InteractionContextType - - Represents the context in which an application command can be used. - - See the :ddocs:`official documentation ` for more info. - - .. versionadded:: 2.10 - - .. attribute:: guild - - Represents a command that's usable in guilds. - .. attribute:: bot_dm - - Represents a command that's usable in DMs with the bot. - .. attribute:: private_channel - - Represents a command that's usable in DMs and group DMs with other users. - Events ------ From 312277ef34e194cf08995bf89a496a004722b5c2 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 16 Sep 2024 19:06:02 +0200 Subject: [PATCH 18/36] refactor: change `oauth_url` `integration_type` default to `MISSING` --- disnake/appinfo.py | 4 +++- disnake/utils.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/disnake/appinfo.py b/disnake/appinfo.py index 058042125b..a00dba6d09 100644 --- a/disnake/appinfo.py +++ b/disnake/appinfo.py @@ -77,7 +77,9 @@ def to_url(self) -> str: self._app_id, scopes=self.scopes, permissions=self.permissions, - integration_type=self._integration_type, + integration_type=( + self._integration_type if self._integration_type is not None else utils.MISSING + ), ) diff --git a/disnake/utils.py b/disnake/utils.py index 419d075b27..88fc4aeb29 100644 --- a/disnake/utils.py +++ b/disnake/utils.py @@ -290,7 +290,7 @@ def oauth_url( redirect_uri: str = MISSING, scopes: Iterable[str] = MISSING, disable_guild_select: bool = False, - integration_type: Optional[ApplicationIntegrationTypeLiteral] = None, + integration_type: ApplicationIntegrationTypeLiteral = MISSING, ) -> str: """A helper function that returns the OAuth2 URL for authorizing the application. @@ -315,7 +315,7 @@ def oauth_url( .. versionadded:: 2.0 - integration_type: Optional[:class:`int`] + integration_type: :class:`int` An optional integration type/installation context to install the application in. .. versionadded:: 2.10 @@ -335,7 +335,7 @@ def oauth_url( url += "&response_type=code&" + urlencode({"redirect_uri": redirect_uri}) if disable_guild_select: url += "&disable_guild_select=true" - if integration_type is not None: + if integration_type is not MISSING: url += f"&integration_type={integration_type}" return url From b7f5cde5b056a6aa199d900187392d66c578ce05 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 16 Sep 2024 19:06:30 +0200 Subject: [PATCH 19/36] test: update `oauth_url` test --- tests/test_utils.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 46237c2019..96c5c4bb5e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -95,17 +95,21 @@ def stuff(num: int) -> int: @pytest.mark.parametrize( - ("expected", "perms", "guild", "redirect", "scopes", "disable_select"), + ("params", "expected"), [ ( + {}, {"scope": "bot"}, - utils.MISSING, - utils.MISSING, - utils.MISSING, - utils.MISSING, - False, ), ( + { + "permissions": disnake.Permissions(42), + "guild": disnake.Object(9999), + "redirect_uri": "http://endless.horse", + "scopes": ["bot", "applications.commands"], + "disable_guild_select": True, + "integration_type": 1, + }, { "scope": "bot applications.commands", "permissions": "42", @@ -113,24 +117,13 @@ def stuff(num: int) -> int: "response_type": "code", "redirect_uri": "http://endless.horse", "disable_guild_select": "true", + "integration_type": "1", }, - disnake.Permissions(42), - disnake.Object(9999), - "http://endless.horse", - ["bot", "applications.commands"], - True, ), ], ) -def test_oauth_url(expected, perms, guild, redirect, scopes, disable_select) -> None: - url = utils.oauth_url( - 1234, - permissions=perms, - guild=guild, - redirect_uri=redirect, - scopes=scopes, - disable_guild_select=disable_select, - ) +def test_oauth_url(params, expected) -> None: + url = utils.oauth_url(1234, **params) assert dict(yarl.URL(url).query) == {"client_id": "1234", **expected} From 0596bee7960c5d31e4dafa490bc728f28bd9d499 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 16 Sep 2024 19:10:20 +0200 Subject: [PATCH 20/36] feat(flags): add `.all()` classmethod to new flag types --- disnake/flags.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/disnake/flags.py b/disnake/flags.py index d8a9171549..2e04594720 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -2234,14 +2234,14 @@ def __init__( ... @classmethod - def all(cls: Type[AutoModKeywordPresets]) -> AutoModKeywordPresets: + def all(cls) -> Self: """A factory method that creates a :class:`AutoModKeywordPresets` with everything enabled.""" self = cls.__new__(cls) self.value = all_flags_value(cls.VALID_FLAGS) return self @classmethod - def none(cls: Type[AutoModKeywordPresets]) -> AutoModKeywordPresets: + def none(cls) -> Self: """A factory method that creates a :class:`AutoModKeywordPresets` with everything disabled.""" self = cls.__new__(cls) self.value = self.DEFAULT_VALUE @@ -2703,6 +2703,13 @@ class ApplicationIntegrationType(ListBaseFlags): def __init__(self, *, guild: bool = ..., user: bool = ...) -> None: ... + @classmethod + def all(cls) -> Self: + """A factory method that creates a :class:`ApplicationIntegrationType` with everything enabled.""" + self = cls.__new__(cls) + self.value = all_flags_value(cls.VALID_FLAGS) + return self + # TODO: `guild` vs `guild_install` @flag_value def guild(self): @@ -2793,6 +2800,13 @@ def __init__( ) -> None: ... + @classmethod + def all(cls) -> Self: + """A factory method that creates a :class:`InteractionContextType` with everything enabled.""" + self = cls.__new__(cls) + self.value = all_flags_value(cls.VALID_FLAGS) + return self + @flag_value def guild(self): """:class:`bool`: Returns ``True`` if the command is usable in guilds.""" From 03d879d2a259f0c92d1456006efbb84e26577849 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sun, 29 Sep 2024 16:47:34 +0200 Subject: [PATCH 21/36] refactor: pluralize flag class names --- disnake/app_commands.py | 80 ++++++++++---------- disnake/appinfo.py | 4 +- disnake/ext/commands/base_core.py | 10 +-- disnake/ext/commands/ctx_menus_core.py | 30 ++++---- disnake/ext/commands/interaction_bot_base.py | 32 ++++---- disnake/ext/commands/slash_core.py | 16 ++-- disnake/flags.py | 70 ++++++++--------- disnake/interactions/application_command.py | 8 +- disnake/interactions/base.py | 14 ++-- disnake/interactions/message.py | 8 +- disnake/interactions/modal.py | 8 +- disnake/message.py | 2 +- docs/api/app_commands.rst | 16 ++-- 13 files changed, 149 insertions(+), 149 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index 041b2d8019..3e155c422f 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -17,7 +17,7 @@ try_enum, try_enum_to_int, ) -from .flags import ApplicationIntegrationType, InteractionContextType +from .flags import ApplicationIntegrationTypes, InteractionContextTypes from .i18n import Localized from .permissions import Permissions from .utils import MISSING, _get_as_snowflake, _maybe_cast @@ -501,14 +501,14 @@ class ApplicationCommand(ABC): .. versionadded:: 2.8 - integration_types: Optional[:class:`ApplicationIntegrationType`] + integration_types: Optional[:class:`ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`ApplicationIntegrationType.guild` only. + Defaults to :attr:`ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`InteractionContextType`] + contexts: Optional[:class:`InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -531,8 +531,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, ) -> None: self.type: ApplicationCommandType = enum_if_int(ApplicationCommandType, type) @@ -556,9 +556,9 @@ def __init__( # XXX: is this actually optional? reconsider the default/fallback value here. self.integration_types: Optional[ - ApplicationIntegrationType - ] = integration_types or ApplicationIntegrationType(guild=True) - self.contexts: Optional[InteractionContextType] = contexts + ApplicationIntegrationTypes + ] = integration_types or ApplicationIntegrationTypes(guild=True) + self.contexts: Optional[InteractionContextTypes] = contexts self._always_synced: bool = False @@ -587,7 +587,7 @@ def default_member_permissions(self) -> Optional[Permissions]: def dm_permission(self): # check if contexts are in use at all if self.contexts: - return InteractionContextType.bot_dm in self.contexts + return InteractionContextTypes.bot_dm in self.contexts return self._dm_permission @@ -697,14 +697,14 @@ class UserCommand(ApplicationCommand): .. versionadded:: 2.8 - integration_types: Optional[:class:`ApplicationIntegrationType`] + integration_types: Optional[:class:`ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`ApplicationIntegrationType.guild` only. + Defaults to :attr:`ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`InteractionContextType`] + contexts: Optional[:class:`InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -719,8 +719,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, ) -> None: super().__init__( type=ApplicationCommandType.user, @@ -760,14 +760,14 @@ class APIUserCommand(UserCommand, _APIApplicationCommandMixin): .. versionadded:: 2.8 - integration_types: Optional[:class:`ApplicationIntegrationType`] + integration_types: Optional[:class:`ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`ApplicationIntegrationType.guild` only. + Defaults to :attr:`ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`InteractionContextType`] + contexts: Optional[:class:`InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -797,12 +797,12 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), integration_types=( - ApplicationIntegrationType._from_values(integration_types) + ApplicationIntegrationTypes._from_values(integration_types) if (integration_types := data.get("integration_types")) is not None else None ), contexts=( - InteractionContextType._from_values(contexts) + InteractionContextTypes._from_values(contexts) if (contexts := data.get("contexts")) is not None else None ), @@ -838,14 +838,14 @@ class MessageCommand(ApplicationCommand): .. versionadded:: 2.8 - integration_types: Optional[:class:`ApplicationIntegrationType`] + integration_types: Optional[:class:`ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`ApplicationIntegrationType.guild` only. + Defaults to :attr:`ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`InteractionContextType`] + contexts: Optional[:class:`InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -860,8 +860,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, ) -> None: super().__init__( type=ApplicationCommandType.message, @@ -901,14 +901,14 @@ class APIMessageCommand(MessageCommand, _APIApplicationCommandMixin): .. versionadded:: 2.8 - integration_types: Optional[:class:`ApplicationIntegrationType`] + integration_types: Optional[:class:`ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`ApplicationIntegrationType.guild` only. + Defaults to :attr:`ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`InteractionContextType`] + contexts: Optional[:class:`InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -938,12 +938,12 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), integration_types=( - ApplicationIntegrationType._from_values(integration_types) + ApplicationIntegrationTypes._from_values(integration_types) if (integration_types := data.get("integration_types")) is not None else None ), contexts=( - InteractionContextType._from_values(contexts) + InteractionContextTypes._from_values(contexts) if (contexts := data.get("contexts")) is not None else None ), @@ -986,14 +986,14 @@ class SlashCommand(ApplicationCommand): .. versionadded:: 2.8 - integration_types: Optional[:class:`ApplicationIntegrationType`] + integration_types: Optional[:class:`ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`ApplicationIntegrationType.guild` only. + Defaults to :attr:`ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`InteractionContextType`] + contexts: Optional[:class:`InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -1016,8 +1016,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, ) -> None: super().__init__( type=ApplicationCommandType.chat_input, @@ -1133,14 +1133,14 @@ class APISlashCommand(SlashCommand, _APIApplicationCommandMixin): .. versionadded:: 2.8 - integration_types: Optional[:class:`ApplicationIntegrationType`] + integration_types: Optional[:class:`ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`ApplicationIntegrationType.guild` only. + Defaults to :attr:`ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`InteractionContextType`] + contexts: Optional[:class:`InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -1176,12 +1176,12 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), integration_types=( - ApplicationIntegrationType._from_values(integration_types) + ApplicationIntegrationTypes._from_values(integration_types) if (integration_types := data.get("integration_types")) is not None else None ), contexts=( - InteractionContextType._from_values(contexts) + InteractionContextTypes._from_values(contexts) if (contexts := data.get("contexts")) is not None else None ), diff --git a/disnake/appinfo.py b/disnake/appinfo.py index a00dba6d09..16ab56bbbb 100644 --- a/disnake/appinfo.py +++ b/disnake/appinfo.py @@ -206,10 +206,10 @@ class AppInfo: (for user-installable apps). .. versionadded:: 2.10 - integration_types_config: Dict[:class:`ApplicationIntegrationType`, :class:`IntegrationTypeConfiguration`] + integration_types_config: Dict[:class:`ApplicationIntegrationTypes`, :class:`IntegrationTypeConfiguration`] The mapping of integration types/installation contexts to their respective configuration parameters. - For example, an :attr:`ApplicationIntegrationType.guild` key being present means that + For example, an :attr:`ApplicationIntegrationTypes.guild` key being present means that the application can be installed to guilds, and its value is the configuration (e.g. scopes, permissions) that will be used during installation, if any. diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 2ae2409040..055c4dee83 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -22,7 +22,7 @@ from disnake.app_commands import ApplicationCommand from disnake.enums import ApplicationCommandType -from disnake.flags import ApplicationIntegrationType, InteractionContextType +from disnake.flags import ApplicationIntegrationTypes, InteractionContextTypes from disnake.permissions import Permissions from disnake.utils import _generated, _overload_with_permissions, async_all, maybe_coroutine @@ -251,8 +251,8 @@ def default_member_permissions(self) -> Optional[Permissions]: return self.body.default_member_permissions @property - def integration_types(self) -> Optional[ApplicationIntegrationType]: - """Optional[:class:`.ApplicationIntegrationType`]: The integration types/installation contexts + def integration_types(self) -> Optional[ApplicationIntegrationTypes]: + """Optional[:class:`.ApplicationIntegrationTypes`]: The integration types/installation contexts where the command is available. Only available for global commands. .. versionadded:: 2.10 @@ -260,8 +260,8 @@ def integration_types(self) -> Optional[ApplicationIntegrationType]: return self.body.integration_types @property - def contexts(self) -> Optional[InteractionContextType]: - """Optional[:class:`.InteractionContextType`]: The interaction contexts + def contexts(self) -> Optional[InteractionContextTypes]: + """Optional[:class:`.InteractionContextTypes`]: The interaction contexts where the command can be used. Only available for global commands. .. versionadded:: 2.10 diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 86023d7541..c46d23a7e6 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Tuple, Union from disnake.app_commands import MessageCommand, UserCommand -from disnake.flags import ApplicationIntegrationType, InteractionContextType +from disnake.flags import ApplicationIntegrationTypes, InteractionContextTypes from disnake.i18n import Localized from disnake.permissions import Permissions @@ -77,8 +77,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, **kwargs: Any, @@ -187,8 +187,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, **kwargs: Any, @@ -251,8 +251,8 @@ def user_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -283,14 +283,14 @@ def user_command( .. versionadded:: 2.8 - integration_types: Optional[:class:`.ApplicationIntegrationType`] + integration_types: Optional[:class:`.ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Defaults to :attr:`.ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`.InteractionContextType`] + contexts: Optional[:class:`.InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -347,8 +347,8 @@ def message_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -382,14 +382,14 @@ def message_command( .. versionadded:: 2.8 - integration_types: Optional[:class:`.ApplicationIntegrationType`] + integration_types: Optional[:class:`.ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Defaults to :attr:`.ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`.InteractionContextType`] + contexts: Optional[:class:`.InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 6a0a407f9c..660730be8b 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -28,7 +28,7 @@ from disnake.app_commands import ApplicationCommand, Option from disnake.custom_warnings import SyncWarning from disnake.enums import ApplicationCommandType -from disnake.flags import ApplicationIntegrationType, InteractionContextType +from disnake.flags import ApplicationIntegrationTypes, InteractionContextTypes from disnake.utils import warn_deprecated from . import errors @@ -490,8 +490,8 @@ def slash_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, options: Optional[List[Option]] = None, guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, @@ -534,14 +534,14 @@ def slash_command( .. versionadded:: 2.8 - integration_types: Optional[:class:`.ApplicationIntegrationType`] + integration_types: Optional[:class:`.ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Defaults to :attr:`.ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`.InteractionContextType`] + contexts: Optional[:class:`.InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -600,8 +600,8 @@ def user_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -635,14 +635,14 @@ def user_command( .. versionadded:: 2.8 - integration_types: Optional[:class:`.ApplicationIntegrationType`] + integration_types: Optional[:class:`.ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Defaults to :attr:`.ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`.InteractionContextType`] + contexts: Optional[:class:`.InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. @@ -694,8 +694,8 @@ def message_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, guild_ids: Optional[Sequence[int]] = None, auto_sync: Optional[bool] = None, extras: Optional[Dict[str, Any]] = None, @@ -729,14 +729,14 @@ def message_command( .. versionadded:: 2.8 - integration_types: Optional[:class:`.ApplicationIntegrationType`] + integration_types: Optional[:class:`.ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Defaults to :attr:`.ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`.InteractionContextType`] + contexts: Optional[:class:`.InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index 4582aa7dec..8d787d550f 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -20,7 +20,7 @@ from disnake import utils from disnake.app_commands import Option, SlashCommand from disnake.enums import OptionType -from disnake.flags import ApplicationIntegrationType, InteractionContextType +from disnake.flags import ApplicationIntegrationTypes, InteractionContextTypes from disnake.i18n import Localized from disnake.interactions import ApplicationCommandInteraction from disnake.permissions import Permissions @@ -437,8 +437,8 @@ def __init__( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, auto_sync: Optional[bool] = None, @@ -758,8 +758,8 @@ def slash_command( dm_permission: Optional[bool] = None, default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, - integration_types: Optional[ApplicationIntegrationType] = None, - contexts: Optional[InteractionContextType] = None, + integration_types: Optional[ApplicationIntegrationTypes] = None, + contexts: Optional[InteractionContextTypes] = None, options: Optional[List[Option]] = None, guild_ids: Optional[Sequence[int]] = None, connectors: Optional[Dict[str, str]] = None, @@ -791,14 +791,14 @@ def slash_command( .. versionadded:: 2.8 - integration_types: Optional[:class:`.ApplicationIntegrationType`] + integration_types: Optional[:class:`.ApplicationIntegrationTypes`] The integration types/installation contexts where the command is available. - Defaults to :attr:`.ApplicationIntegrationType.guild` only. + Defaults to :attr:`.ApplicationIntegrationTypes.guild` only. Only available for global commands. .. versionadded:: 2.10 - contexts: Optional[:class:`.InteractionContextType`] + contexts: Optional[:class:`.InteractionContextTypes`] The interaction contexts where the command can be used. Only available for global commands. diff --git a/disnake/flags.py b/disnake/flags.py index 2e04594720..e593bd0a3c 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -43,8 +43,8 @@ "RoleFlags", "AttachmentFlags", "SKUFlags", - "ApplicationIntegrationType", - "InteractionContextType", + "ApplicationIntegrationTypes", + "InteractionContextTypes", ) BF = TypeVar("BF", bound="BaseFlags") @@ -2627,7 +2627,7 @@ def user_subscription(self): return 1 << 8 -class ApplicationIntegrationType(ListBaseFlags): +class ApplicationIntegrationTypes(ListBaseFlags): """Represents the location(s) in which an application or application command can be installed. See the :ddocs:`official documentation ` for more info. @@ -2636,37 +2636,37 @@ class ApplicationIntegrationType(ListBaseFlags): .. describe:: x == y - Checks if two ApplicationIntegrationType instances are equal. + Checks if two ApplicationIntegrationTypes instances are equal. .. describe:: x != y - Checks if two ApplicationIntegrationType instances are not equal. + Checks if two ApplicationIntegrationTypes instances are not equal. .. describe:: x <= y - Checks if an ApplicationIntegrationType instance is a subset of another ApplicationIntegrationType instance. + Checks if an ApplicationIntegrationTypes instance is a subset of another ApplicationIntegrationTypes instance. .. describe:: x >= y - Checks if an ApplicationIntegrationType instance is a superset of another ApplicationIntegrationType instance. + Checks if an ApplicationIntegrationTypes instance is a superset of another ApplicationIntegrationTypes instance. .. describe:: x < y - Checks if an ApplicationIntegrationType instance is a strict subset of another ApplicationIntegrationType instance. + Checks if an ApplicationIntegrationTypes instance is a strict subset of another ApplicationIntegrationTypes instance. .. describe:: x > y - Checks if an ApplicationIntegrationType instance is a strict superset of another ApplicationIntegrationType instance. + Checks if an ApplicationIntegrationTypes instance is a strict superset of another ApplicationIntegrationTypes instance. .. describe:: x | y, x |= y - Returns a new ApplicationIntegrationType instance with all enabled flags from both x and y. + Returns a new ApplicationIntegrationTypes instance with all enabled flags from both x and y. (Using ``|=`` will update in place). .. describe:: x & y, x &= y - Returns a new ApplicationIntegrationType instance with only flags enabled on both x and y. + Returns a new ApplicationIntegrationTypes instance with only flags enabled on both x and y. (Using ``&=`` will update in place). .. describe:: x ^ y, x ^= y - Returns a new ApplicationIntegrationType instance with only flags enabled on one of x or y, but not both. + Returns a new ApplicationIntegrationTypes instance with only flags enabled on one of x or y, but not both. (Using ``^=`` will update in place). .. describe:: ~x - Returns a new ApplicationIntegrationType instance with all flags from x inverted. + Returns a new ApplicationIntegrationTypes instance with all flags from x inverted. .. describe:: hash(x) Returns the flag's hash. @@ -2678,13 +2678,13 @@ class ApplicationIntegrationType(ListBaseFlags): Additionally supported are a few operations on class attributes. - .. describe:: ApplicationIntegrationType.y | ApplicationIntegrationType.z, ApplicationIntegrationType(y=True) | ApplicationIntegrationType.z + .. describe:: ApplicationIntegrationTypes.y | ApplicationIntegrationTypes.z, ApplicationIntegrationTypes(y=True) | ApplicationIntegrationTypes.z - Returns a ApplicationIntegrationType instance with all provided flags enabled. + Returns a ApplicationIntegrationTypes instance with all provided flags enabled. - .. describe:: ~ApplicationIntegrationType.y + .. describe:: ~ApplicationIntegrationTypes.y - Returns a ApplicationIntegrationType instance with all flags except ``y`` inverted from their default value. + Returns a ApplicationIntegrationTypes instance with all flags except ``y`` inverted from their default value. .. versionadded:: 2.10 @@ -2705,7 +2705,7 @@ def __init__(self, *, guild: bool = ..., user: bool = ...) -> None: @classmethod def all(cls) -> Self: - """A factory method that creates a :class:`ApplicationIntegrationType` with everything enabled.""" + """A factory method that creates a :class:`ApplicationIntegrationTypes` with everything enabled.""" self = cls.__new__(cls) self.value = all_flags_value(cls.VALID_FLAGS) return self @@ -2722,8 +2722,8 @@ def user(self): return 1 << 1 -class InteractionContextType(ListBaseFlags): - """Represents the context in which an application command can be used. +class InteractionContextTypes(ListBaseFlags): + """Represents the context(s) in which an application command can be used. See the :ddocs:`official documentation ` for more info. @@ -2731,37 +2731,37 @@ class InteractionContextType(ListBaseFlags): .. describe:: x == y - Checks if two InteractionContextType instances are equal. + Checks if two InteractionContextTypes instances are equal. .. describe:: x != y - Checks if two InteractionContextType instances are not equal. + Checks if two InteractionContextTypes instances are not equal. .. describe:: x <= y - Checks if an InteractionContextType instance is a subset of another InteractionContextType instance. + Checks if an InteractionContextTypes instance is a subset of another InteractionContextTypes instance. .. describe:: x >= y - Checks if an InteractionContextType instance is a superset of another InteractionContextType instance. + Checks if an InteractionContextTypes instance is a superset of another InteractionContextTypes instance. .. describe:: x < y - Checks if an InteractionContextType instance is a strict subset of another InteractionContextType instance. + Checks if an InteractionContextTypes instance is a strict subset of another InteractionContextTypes instance. .. describe:: x > y - Checks if an InteractionContextType instance is a strict superset of another InteractionContextType instance. + Checks if an InteractionContextTypes instance is a strict superset of another InteractionContextTypes instance. .. describe:: x | y, x |= y - Returns a new InteractionContextType instance with all enabled flags from both x and y. + Returns a new InteractionContextTypes instance with all enabled flags from both x and y. (Using ``|=`` will update in place). .. describe:: x & y, x &= y - Returns a new InteractionContextType instance with only flags enabled on both x and y. + Returns a new InteractionContextTypes instance with only flags enabled on both x and y. (Using ``&=`` will update in place). .. describe:: x ^ y, x ^= y - Returns a new InteractionContextType instance with only flags enabled on one of x or y, but not both. + Returns a new InteractionContextTypes instance with only flags enabled on one of x or y, but not both. (Using ``^=`` will update in place). .. describe:: ~x - Returns a new InteractionContextType instance with all flags from x inverted. + Returns a new InteractionContextTypes instance with all flags from x inverted. .. describe:: hash(x) Returns the flag's hash. @@ -2773,13 +2773,13 @@ class InteractionContextType(ListBaseFlags): Additionally supported are a few operations on class attributes. - .. describe:: InteractionContextType.y | InteractionContextType.z, InteractionContextType(y=True) | InteractionContextType.z + .. describe:: InteractionContextTypes.y | InteractionContextTypes.z, InteractionContextTypes(y=True) | InteractionContextTypes.z - Returns a InteractionContextType instance with all provided flags enabled. + Returns a InteractionContextTypes instance with all provided flags enabled. - .. describe:: ~InteractionContextType.y + .. describe:: ~InteractionContextTypes.y - Returns a InteractionContextType instance with all flags except ``y`` inverted from their default value. + Returns a InteractionContextTypes instance with all flags except ``y`` inverted from their default value. .. versionadded:: 2.10 @@ -2802,7 +2802,7 @@ def __init__( @classmethod def all(cls) -> Self: - """A factory method that creates a :class:`InteractionContextType` with everything enabled.""" + """A factory method that creates a :class:`InteractionContextTypes` with everything enabled.""" self = cls.__new__(cls) self.value = all_flags_value(cls.VALID_FLAGS) return self diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 5360b468f0..ba1d2d2bd2 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -105,16 +105,16 @@ class ApplicationCommandInteraction(Interaction[ClientT]): .. versionadded:: 2.10 - authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationTypes`, int] The authorizing user/guild for the application installation. This is only available if the application was installed to a user, and is empty otherwise. If this interaction was triggered through an application command, this requirement also applies to the command itself; see :attr:`ApplicationCommand.integration_types`. - The value for the :attr:`ApplicationIntegrationType.user` key is the user ID. + The value for the :attr:`ApplicationIntegrationTypes.user` key is the user ID. If the application (and command) was also installed to the guild, the value for the - :attr:`ApplicationIntegrationType.guild` key is the guild ID, or ``0`` in DMs with the bot. + :attr:`ApplicationIntegrationTypes.guild` key is the guild ID, or ``0`` in DMs with the bot. See the :ddocs:`official docs ` for more information. @@ -124,7 +124,7 @@ class ApplicationCommandInteraction(Interaction[ClientT]): .. versionadded:: 2.10 - context: Optional[:class:`InteractionContextType`] + context: Optional[:class:`InteractionContextTypes`] The context where the interaction was triggered from. This has the same requirements as :attr:`authorizing_integration_owners`; that is, diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index 4e1a5fc575..c95e4afab1 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -41,7 +41,7 @@ ModalChainNotSupported, NotFound, ) -from ..flags import InteractionContextType, MessageFlags +from ..flags import InteractionContextTypes, MessageFlags from ..guild import Guild from ..i18n import Localized from ..member import Member @@ -163,16 +163,16 @@ class Interaction(Generic[ClientT]): .. versionadded:: 2.10 - authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationTypes`, int] The authorizing user/guild for the application installation. This is only available if the application was installed to a user, and is empty otherwise. If this interaction was triggered through an application command, this requirement also applies to the command itself; see :attr:`ApplicationCommand.integration_types`. - The value for the :attr:`ApplicationIntegrationType.user` key is the user ID. + The value for the :attr:`ApplicationIntegrationTypes.user` key is the user ID. If the application (and command) was also installed to the guild, the value for the - :attr:`ApplicationIntegrationType.guild` key is the guild ID, or ``0`` in DMs with the bot. + :attr:`ApplicationIntegrationTypes.guild` key is the guild ID, or ``0`` in DMs with the bot. See the :ddocs:`official docs ` for more information. @@ -182,7 +182,7 @@ class Interaction(Generic[ClientT]): .. versionadded:: 2.10 - context: Optional[:class:`InteractionContextType`] + context: Optional[:class:`InteractionContextTypes`] The context where the interaction was triggered from. This has the same requirements as :attr:`authorizing_integration_owners`; that is, @@ -282,8 +282,8 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState) -> None: } # TODO: document this properly; it's a flag object, but only one value will be set - self.context: Optional[InteractionContextType] = ( - InteractionContextType._from_values([context]) + self.context: Optional[InteractionContextTypes] = ( + InteractionContextTypes._from_values([context]) if (context := data.get("context")) is not None else None ) diff --git a/disnake/interactions/message.py b/disnake/interactions/message.py index 0f97611dd5..771206d664 100644 --- a/disnake/interactions/message.py +++ b/disnake/interactions/message.py @@ -92,16 +92,16 @@ class MessageInteraction(Interaction[ClientT]): .. versionadded:: 2.10 - authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationTypes`, int] The authorizing user/guild for the application installation. This is only available if the application was installed to a user, and is empty otherwise. If this interaction was triggered through an application command, this requirement also applies to the command itself; see :attr:`ApplicationCommand.integration_types`. - The value for the :attr:`ApplicationIntegrationType.user` key is the user ID. + The value for the :attr:`ApplicationIntegrationTypes.user` key is the user ID. If the application (and command) was also installed to the guild, the value for the - :attr:`ApplicationIntegrationType.guild` key is the guild ID, or ``0`` in DMs with the bot. + :attr:`ApplicationIntegrationTypes.guild` key is the guild ID, or ``0`` in DMs with the bot. See the :ddocs:`official docs ` for more information. @@ -111,7 +111,7 @@ class MessageInteraction(Interaction[ClientT]): .. versionadded:: 2.10 - context: Optional[:class:`InteractionContextType`] + context: Optional[:class:`InteractionContextTypes`] The context where the interaction was triggered from. This has the same requirements as :attr:`authorizing_integration_owners`; that is, diff --git a/disnake/interactions/modal.py b/disnake/interactions/modal.py index 67b7874c4a..93ed2faded 100644 --- a/disnake/interactions/modal.py +++ b/disnake/interactions/modal.py @@ -80,16 +80,16 @@ class ModalInteraction(Interaction[ClientT]): .. versionadded:: 2.10 - authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationTypes`, int] The authorizing user/guild for the application installation. This is only available if the application was installed to a user, and is empty otherwise. If this interaction was triggered through an application command, this requirement also applies to the command itself; see :attr:`ApplicationCommand.integration_types`. - The value for the :attr:`ApplicationIntegrationType.user` key is the user ID. + The value for the :attr:`ApplicationIntegrationTypes.user` key is the user ID. If the application (and command) was also installed to the guild, the value for the - :attr:`ApplicationIntegrationType.guild` key is the guild ID, or ``0`` in DMs with the bot. + :attr:`ApplicationIntegrationTypes.guild` key is the guild ID, or ``0`` in DMs with the bot. See the :ddocs:`official docs ` for more information. @@ -99,7 +99,7 @@ class ModalInteraction(Interaction[ClientT]): .. versionadded:: 2.10 - context: Optional[:class:`InteractionContextType`] + context: Optional[:class:`InteractionContextTypes`] The context where the interaction was triggered from. This has the same requirements as :attr:`authorizing_integration_owners`; that is, diff --git a/disnake/message.py b/disnake/message.py index 10ece00332..0d9b7120b6 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -773,7 +773,7 @@ class InteractionMetadata: The type of the interaction. user: :class:`User` The user that triggered the interaction. - authorizing_integration_owners: Dict[:class:`ApplicationIntegrationType`, int] + authorizing_integration_owners: Dict[:class:`ApplicationIntegrationTypes`, int] The authorizing user/guild for the application installation related to the interaction. See :attr:`Interaction.authorizing_integration_owners` for details. original_response_message_id: Optional[:class:`int`] diff --git a/docs/api/app_commands.rst b/docs/api/app_commands.rst index 2aa9e31bbc..38f843ae9f 100644 --- a/docs/api/app_commands.rst +++ b/docs/api/app_commands.rst @@ -108,20 +108,20 @@ OptionChoice .. autoclass:: OptionChoice() :members: -ApplicationIntegrationType -~~~~~~~~~~~~~~~~~~~~~~~~~~ +ApplicationIntegrationTypes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. attributetable:: ApplicationIntegrationType +.. attributetable:: ApplicationIntegrationTypes -.. autoclass:: ApplicationIntegrationType() +.. autoclass:: ApplicationIntegrationTypes() :members: -InteractionContextType -~~~~~~~~~~~~~~~~~~~~~~ +InteractionContextTypes +~~~~~~~~~~~~~~~~~~~~~~~ -.. attributetable:: InteractionContextType +.. attributetable:: InteractionContextTypes -.. autoclass:: InteractionContextType() +.. autoclass:: InteractionContextTypes() :members: Enumerations From 77ccd42dc5d0f4e9ab8db749a156700464f5db3f Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sun, 29 Sep 2024 16:57:52 +0200 Subject: [PATCH 22/36] fix(docs): a/an grammar stuff in flags --- disnake/flags.py | 64 ++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/disnake/flags.py b/disnake/flags.py index e593bd0a3c..3a9cb616da 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -997,12 +997,12 @@ class Intents(BaseFlags): .. describe:: Intents.y | Intents.z, Intents(y=True) | Intents.z - Returns a Intents instance with all provided flags enabled. + Returns an Intents instance with all provided flags enabled. .. versionadded:: 2.6 .. describe:: ~Intents.y - Returns a Intents instance with all flags except ``y`` inverted from their default value. + Returns an Intents instance with all flags except ``y`` inverted from their default value. .. versionadded:: 2.6 @@ -1079,21 +1079,21 @@ def __init__(self, value: Optional[int] = None, **kwargs: bool) -> None: @classmethod def all(cls) -> Self: - """A factory method that creates a :class:`Intents` with everything enabled.""" + """A factory method that creates an :class:`Intents` instance 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:`Intents` with everything disabled.""" + """A factory method that creates an :class:`Intents` instance 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:`Intents` with everything enabled + """A factory method that creates an :class:`Intents` instance with everything enabled except :attr:`presences`, :attr:`members`, and :attr:`message_content`. """ self = cls.all() @@ -1780,14 +1780,14 @@ def __init__(self, **kwargs: bool) -> None: @classmethod def all(cls) -> Self: - """A factory method that creates a :class:`MemberCacheFlags` with everything enabled.""" + """A factory method that creates a :class:`MemberCacheFlags` instance 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:`MemberCacheFlags` with everything disabled.""" + """A factory method that creates a :class:`MemberCacheFlags` instance with everything disabled.""" self = cls.__new__(cls) self.value = self.DEFAULT_VALUE return self @@ -1819,7 +1819,7 @@ def joined(self) -> int: @classmethod def from_intents(cls, intents: Intents) -> Self: - """A factory method that creates a :class:`MemberCacheFlags` based on + """A factory method that creates a :class:`MemberCacheFlags` instance based on the currently selected :class:`Intents`. Parameters @@ -1920,12 +1920,12 @@ class ApplicationFlags(BaseFlags): .. describe:: ApplicationFlags.y | ApplicationFlags.z, ApplicationFlags(y=True) | ApplicationFlags.z - Returns a ApplicationFlags instance with all provided flags enabled. + Returns an ApplicationFlags instance with all provided flags enabled. .. versionadded:: 2.6 .. describe:: ~ApplicationFlags.y - Returns a ApplicationFlags instance with all flags except ``y`` inverted from their default value. + Returns an ApplicationFlags instance with all flags except ``y`` inverted from their default value. .. versionadded:: 2.6 @@ -2208,11 +2208,11 @@ class AutoModKeywordPresets(ListBaseFlags): .. describe:: AutoModKeywordPresets.y | AutoModKeywordPresets.z, AutoModKeywordPresets(y=True) | AutoModKeywordPresets.z - Returns a AutoModKeywordPresets instance with all provided flags enabled. + Returns an AutoModKeywordPresets instance with all provided flags enabled. .. describe:: ~AutoModKeywordPresets.y - Returns a AutoModKeywordPresets instance with all flags except ``y`` inverted from their default value. + Returns an AutoModKeywordPresets instance with all flags except ``y`` inverted from their default value. .. versionadded:: 2.6 @@ -2235,14 +2235,14 @@ def __init__( @classmethod def all(cls) -> Self: - """A factory method that creates a :class:`AutoModKeywordPresets` with everything enabled.""" + """A factory method that creates an :class:`AutoModKeywordPresets` instance 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:`AutoModKeywordPresets` with everything disabled.""" + """A factory method that creates an :class:`AutoModKeywordPresets` instance with everything disabled.""" self = cls.__new__(cls) self.value = self.DEFAULT_VALUE return self @@ -2282,16 +2282,16 @@ class MemberFlags(BaseFlags): Checks if two MemberFlags instances are not equal. .. describe:: x <= y - Checks if an MemberFlags instance is a subset of another MemberFlags instance. + Checks if a MemberFlags instance is a subset of another MemberFlags instance. .. describe:: x >= y - Checks if an MemberFlags instance is a superset of another MemberFlags instance. + Checks if a MemberFlags instance is a superset of another MemberFlags instance. .. describe:: x < y - Checks if an MemberFlags instance is a strict subset of another MemberFlags instance. + Checks if a MemberFlags instance is a strict subset of another MemberFlags instance. .. describe:: x > y - Checks if an MemberFlags instance is a strict superset of another MemberFlags instance. + Checks if a MemberFlags instance is a strict superset of another MemberFlags instance. .. describe:: x | y, x |= y Returns a new MemberFlags instance with all enabled flags from both x and y. @@ -2384,16 +2384,16 @@ class RoleFlags(BaseFlags): Checks if two RoleFlags instances are not equal. .. describe:: x <= y - Checks if an RoleFlags instance is a subset of another RoleFlags instance. + Checks if a RoleFlags instance is a subset of another RoleFlags instance. .. describe:: x >= y - Checks if an RoleFlags instance is a superset of another RoleFlags instance. + Checks if a RoleFlags instance is a superset of another RoleFlags instance. .. describe:: x < y - Checks if an RoleFlags instance is a strict subset of another RoleFlags instance. + Checks if a RoleFlags instance is a strict subset of another RoleFlags instance. .. describe:: x > y - Checks if an RoleFlags instance is a strict superset of another RoleFlags instance. + Checks if a RoleFlags instance is a strict superset of another RoleFlags instance. .. describe:: x | y, x |= y Returns a new RoleFlags instance with all enabled flags from both x and y. @@ -2502,11 +2502,11 @@ class AttachmentFlags(BaseFlags): .. describe:: AttachmentFlags.y | AttachmentFlags.z, AttachmentFlags(y=True) | AttachmentFlags.z - Returns a AttachmentFlags instance with all provided flags enabled. + Returns an AttachmentFlags instance with all provided flags enabled. .. describe:: ~AttachmentFlags.y - Returns a AttachmentFlags instance with all flags except ``y`` inverted from their default value. + Returns an AttachmentFlags instance with all flags except ``y`` inverted from their default value. .. versionadded:: 2.10 @@ -2582,11 +2582,11 @@ class SKUFlags(BaseFlags): .. describe:: SKUFlags.y | SKUFlags.z, SKUFlags(y=True) | SKUFlags.z - Returns a SKUFlags instance with all provided flags enabled. + Returns an SKUFlags instance with all provided flags enabled. .. describe:: ~SKUFlags.y - Returns a SKUFlags instance with all flags except ``y`` inverted from their default value. + Returns an SKUFlags instance with all flags except ``y`` inverted from their default value. .. versionadded:: 2.10 @@ -2680,11 +2680,11 @@ class ApplicationIntegrationTypes(ListBaseFlags): .. describe:: ApplicationIntegrationTypes.y | ApplicationIntegrationTypes.z, ApplicationIntegrationTypes(y=True) | ApplicationIntegrationTypes.z - Returns a ApplicationIntegrationTypes instance with all provided flags enabled. + Returns an ApplicationIntegrationTypes instance with all provided flags enabled. .. describe:: ~ApplicationIntegrationTypes.y - Returns a ApplicationIntegrationTypes instance with all flags except ``y`` inverted from their default value. + Returns an ApplicationIntegrationTypes instance with all flags except ``y`` inverted from their default value. .. versionadded:: 2.10 @@ -2705,7 +2705,7 @@ def __init__(self, *, guild: bool = ..., user: bool = ...) -> None: @classmethod def all(cls) -> Self: - """A factory method that creates a :class:`ApplicationIntegrationTypes` with everything enabled.""" + """A factory method that creates an :class:`ApplicationIntegrationTypes` instance with everything enabled.""" self = cls.__new__(cls) self.value = all_flags_value(cls.VALID_FLAGS) return self @@ -2775,11 +2775,11 @@ class InteractionContextTypes(ListBaseFlags): .. describe:: InteractionContextTypes.y | InteractionContextTypes.z, InteractionContextTypes(y=True) | InteractionContextTypes.z - Returns a InteractionContextTypes instance with all provided flags enabled. + Returns an InteractionContextTypes instance with all provided flags enabled. .. describe:: ~InteractionContextTypes.y - Returns a InteractionContextTypes instance with all flags except ``y`` inverted from their default value. + Returns an InteractionContextTypes instance with all flags except ``y`` inverted from their default value. .. versionadded:: 2.10 @@ -2802,7 +2802,7 @@ def __init__( @classmethod def all(cls) -> Self: - """A factory method that creates a :class:`InteractionContextTypes` with everything enabled.""" + """A factory method that creates an :class:`InteractionContextTypes` instance with everything enabled.""" self = cls.__new__(cls) self.value = all_flags_value(cls.VALID_FLAGS) return self From ef18d85429eab500e9fd03ffd36a69baf0708f45 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sun, 29 Sep 2024 16:58:01 +0200 Subject: [PATCH 23/36] chore: yet another smol todo --- disnake/flags.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/disnake/flags.py b/disnake/flags.py index 3a9cb616da..e0eaef3d95 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -2710,6 +2710,8 @@ def all(cls) -> Self: self.value = all_flags_value(cls.VALID_FLAGS) return self + # TODO: none() ? + # TODO: `guild` vs `guild_install` @flag_value def guild(self): From 2b0ab7a509823f562eefe3cab5673041f6ba31a9 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sun, 29 Sep 2024 17:00:28 +0200 Subject: [PATCH 24/36] revert: "fix: make dm_permission a property" This reverts commit 93de3c8726a363095a881fb8f728fc4cd55c7494. --- disnake/app_commands.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index 3e155c422f..ae322c275f 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -541,7 +541,8 @@ def __init__( self.name_localizations: LocalizationValue = name_loc.localizations self.nsfw: bool = False if nsfw is None else nsfw - self._dm_permission: Optional[bool] = dm_permission + # TODO: turn this into a property based on `1 in self.contexts` instead, stop sending dm_permission + self.dm_permission: bool = True if dm_permission is None else dm_permission self._default_member_permissions: Optional[int] if default_member_permissions is None: @@ -583,19 +584,6 @@ def default_member_permissions(self) -> Optional[Permissions]: return None return Permissions(self._default_member_permissions) - @property - def dm_permission(self): - # check if contexts are in use at all - if self.contexts: - return InteractionContextTypes.bot_dm in self.contexts - - return self._dm_permission - - @dm_permission.setter - def dm_permission(self, value: bool) -> None: - # todo: add a deprecation warning here - self._dm_permission = value - def __repr__(self) -> str: attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__) return f"<{type(self).__name__} {attrs}>" @@ -634,6 +622,7 @@ def to_dict(self) -> EditApplicationCommandPayload: if self._default_member_permissions is not None else None ), + "dm_permission": self.dm_permission, "default_permission": True, "nsfw": self.nsfw, } @@ -649,9 +638,6 @@ def to_dict(self) -> EditApplicationCommandPayload: if (loc := self.name_localizations.data) is not None: data["name_localizations"] = loc - if not self.contexts and self.dm_permission is not None: - data["dm_permission"] = self.dm_permission - return data def localize(self, store: LocalizationProtocol) -> None: From d1ed1759c2e2ec7084d507b0ac5bbd204d048e9f Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 30 Sep 2024 14:17:29 +0200 Subject: [PATCH 25/36] refactor: turn `dm_permission` into `contexts.bot_dm`, handle `GuildCommandInteraction` correctly --- disnake/app_commands.py | 129 ++++++++----------- disnake/ext/commands/base_core.py | 8 +- disnake/ext/commands/ctx_menus_core.py | 30 +++-- disnake/ext/commands/interaction_bot_base.py | 21 ++- disnake/ext/commands/slash_core.py | 16 ++- disnake/interactions/application_command.py | 7 +- docs/ext/commands/slash_commands.rst | 3 + 7 files changed, 118 insertions(+), 96 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index ae322c275f..ff4ed96d9e 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -20,7 +20,7 @@ from .flags import ApplicationIntegrationTypes, InteractionContextTypes from .i18n import Localized from .permissions import Permissions -from .utils import MISSING, _get_as_snowflake, _maybe_cast +from .utils import MISSING, _get_as_snowflake, _maybe_cast, deprecated if TYPE_CHECKING: from typing_extensions import Self @@ -486,15 +486,6 @@ class ApplicationCommand(ABC): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - Defaults to ``True``. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -528,7 +519,7 @@ def __init__( self, type: ApplicationCommandType, name: LocalizedRequired, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -541,9 +532,6 @@ def __init__( self.name_localizations: LocalizationValue = name_loc.localizations self.nsfw: bool = False if nsfw is None else nsfw - # TODO: turn this into a property based on `1 in self.contexts` instead, stop sending dm_permission - self.dm_permission: bool = True if dm_permission is None else dm_permission - self._default_member_permissions: Optional[int] if default_member_permissions is None: # allow everyone to use the command if its not supplied @@ -566,6 +554,14 @@ def __init__( # reset `default_permission` if set before self._default_permission: bool = True + # TODO: consider throwing if both `dm_permission` and `contexts` are provided; take `GuildCommandInteraction` into account as well. + + # `dm_permission` is deprecated; this turns it into `contexts.bot_dm`, which is equivalent. + # The API computes `dm_permission` based on `contexts` anyway (if set), so `contexts` is the + # source of truth here; it ignores `dm_permission` if `contexts` is set. + if dm_permission is not None: + self.dm_permission = dm_permission + @property def default_member_permissions(self) -> Optional[Permissions]: """Optional[:class:`Permissions`]: The default required member permissions for this command. @@ -584,6 +580,38 @@ def default_member_permissions(self) -> Optional[Permissions]: return None return Permissions(self._default_member_permissions) + @property + @deprecated("contexts") + def dm_permission(self) -> bool: + """ + Whether this command can be used in DMs with the bot. + + .. versionadded:: 2.5 + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + """ + if self.contexts is not None: + return self.contexts.bot_dm + + return True + + @dm_permission.setter + @deprecated("contexts") + def dm_permission(self, value: bool) -> None: + self._convert_dm_permission(value) + + # this is separate so we can call it internally while avoiding DeprecationWarnings + def _convert_dm_permission(self, value: bool, *, apply_private_channel: bool = False) -> None: + if self.contexts is None: + # this is the default if neither dm_permission nor contexts are set + self.contexts = InteractionContextTypes(guild=True, bot_dm=True) + + self.contexts.bot_dm = value + if apply_private_channel: + self.contexts.private_channel = value + def __repr__(self) -> str: attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__) return f"<{type(self).__name__} {attrs}>" @@ -605,8 +633,8 @@ def __eq__(self, other) -> bool: for obj in (self, other) ) or ( - self.dm_permission == other.dm_permission - and self.integration_types == other.integration_types + self.integration_types == other.integration_types + # TODO: `None` and (guild=True, bot_dm=True) are practically the same and self.contexts == other.contexts ) ) @@ -622,7 +650,6 @@ def to_dict(self) -> EditApplicationCommandPayload: if self._default_member_permissions is not None else None ), - "dm_permission": self.dm_permission, "default_permission": True, "nsfw": self.nsfw, } @@ -648,13 +675,21 @@ class _APIApplicationCommandMixin: __repr_info__ = ("id",) def _update_common(self, data: ApplicationCommandPayload) -> None: + if not isinstance(self, ApplicationCommand): + raise TypeError("_APIApplicationCommandMixin must be used with ApplicationCommand") + self.id: int = int(data["id"]) self.application_id: int = int(data["application_id"]) self.guild_id: Optional[int] = _get_as_snowflake(data, "guild_id") self.version: int = int(data["version"]) + # deprecated, but kept until API stops returning this field self._default_permission = data.get("default_permission") is not False + # same deal, also deprecated + if (dm_permission := data.get("dm_permission")) is not None: + self._convert_dm_permission(dm_permission) + class UserCommand(ApplicationCommand): """A user context menu command. @@ -668,15 +703,6 @@ class UserCommand(ApplicationCommand): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - Defaults to ``True``. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -702,7 +728,7 @@ class UserCommand(ApplicationCommand): def __init__( self, name: LocalizedRequired, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -733,14 +759,6 @@ class APIUserCommand(UserCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. @@ -779,7 +797,6 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: self = cls( name=Localized(data["name"], data=data.get("name_localizations")), - dm_permission=data.get("dm_permission") is not False, default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), integration_types=( @@ -809,15 +826,6 @@ class MessageCommand(ApplicationCommand): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - Defaults to ``True``. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -843,7 +851,7 @@ class MessageCommand(ApplicationCommand): def __init__( self, name: LocalizedRequired, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -874,14 +882,6 @@ class APIMessageCommand(MessageCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. @@ -920,7 +920,6 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: self = cls( name=Localized(data["name"], data=data.get("name_localizations")), - dm_permission=data.get("dm_permission") is not False, default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), integration_types=( @@ -957,15 +956,6 @@ class SlashCommand(ApplicationCommand): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - Defaults to ``True``. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -999,7 +989,7 @@ def __init__( name: LocalizedRequired, description: LocalizedRequired, options: Optional[List[Option]] = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -1106,14 +1096,6 @@ class APISlashCommand(SlashCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. @@ -1158,7 +1140,6 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self: options=_maybe_cast( data.get("options", MISSING), lambda x: list(map(Option.from_dict, x)) ), - dm_permission=data.get("dm_permission") is not False, default_member_permissions=_get_as_snowflake(data, "default_member_permissions"), nsfw=data.get("nsfw"), integration_types=( diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 055c4dee83..3824095601 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -137,7 +137,7 @@ def __init__(self, func: CommandCallback, *, name: Optional[str] = None, **kwarg 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. + # We will use this attribute later in order to set the allowed contexts. self._guild_only: bool = kwargs.get("guild_only", False) self.extras: Dict[str, Any] = kwargs.get("extras") or {} @@ -229,6 +229,12 @@ def _update_copy(self: AppCommandT, kwargs: Dict[str, Any]) -> AppCommandT: else: return self.copy() + def _apply_guild_only(self) -> None: + # If we have a `GuildCommandInteraction` annotation, + # turn it into `contexts.bot_dm = contexts.private_channel = False` + if self._guild_only: + self.body._convert_dm_permission(False, apply_private_channel=True) + @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 c46d23a7e6..2e094998b1 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -74,7 +74,7 @@ def __init__( func: InteractionCommandCallback[CogT, UserCommandInteraction, P], *, name: LocalizedOptional = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -99,17 +99,17 @@ def __init__( ) default_member_permissions = default_perms - dm_permission = True if dm_permission is None else dm_permission - self.body = UserCommand( name=name_loc._upgrade(self.name), - dm_permission=dm_permission and not self._guild_only, + dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, integration_types=integration_types, contexts=contexts, ) + self._apply_guild_only() + async def _call_external_error_handlers( self, inter: ApplicationCommandInteraction, error: CommandError ) -> None: @@ -184,7 +184,7 @@ def __init__( func: InteractionCommandCallback[CogT, MessageCommandInteraction, P], *, name: LocalizedOptional = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -203,17 +203,17 @@ def __init__( except AttributeError: pass - dm_permission = True if dm_permission is None else dm_permission - self.body = MessageCommand( name=name_loc._upgrade(self.name), - dm_permission=dm_permission and not self._guild_only, + dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, integration_types=integration_types, contexts=contexts, ) + self._apply_guild_only() + async def _call_external_error_handlers( self, inter: ApplicationCommandInteraction, error: CommandError ) -> None: @@ -248,7 +248,7 @@ async def __call__( def user_command( *, name: LocalizedOptional = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -271,6 +271,11 @@ def user_command( dm_permission: :class:`bool` Whether this command can be used in DMs. Defaults to ``True``. + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. See :attr:`.ApplicationCommand.default_member_permissions` for details. @@ -344,7 +349,7 @@ def decorator( def message_command( *, name: LocalizedOptional = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -370,6 +375,11 @@ def message_command( dm_permission: :class:`bool` Whether this command can be used in DMs. Defaults to ``True``. + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. See :attr:`.ApplicationCommand.default_member_permissions` for details. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 660730be8b..ce06c8b4a8 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -487,7 +487,7 @@ def slash_command( *, name: LocalizedOptional = None, description: LocalizedOptional = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -522,6 +522,11 @@ def slash_command( dm_permission: :class:`bool` Whether this command can be used in DMs. Defaults to ``True``. + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. See :attr:`.ApplicationCommand.default_member_permissions` for details. @@ -597,7 +602,7 @@ def user_command( self, *, name: LocalizedOptional = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -623,6 +628,11 @@ def user_command( dm_permission: :class:`bool` Whether this command can be used in DMs. Defaults to ``True``. + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. See :attr:`.ApplicationCommand.default_member_permissions` for details. @@ -691,7 +701,7 @@ def message_command( self, *, name: LocalizedOptional = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -717,6 +727,11 @@ def message_command( dm_permission: :class:`bool` Whether this command can be used in DMs. Defaults to ``True``. + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. See :attr:`.ApplicationCommand.default_member_permissions` for details. diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index 8d787d550f..c794923ae5 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -157,6 +157,7 @@ def __init__( ) self.qualified_name: str = f"{parent.qualified_name} {self.name}" + # TODO if ( "dm_permission" in kwargs or "default_member_permissions" in kwargs @@ -434,7 +435,7 @@ def __init__( name: LocalizedOptional = None, description: LocalizedOptional = None, options: Optional[List[Option]] = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -464,21 +465,21 @@ def __init__( except AttributeError: pass - dm_permission = True if dm_permission is None else dm_permission - self.body: SlashCommand = SlashCommand( name=name_loc._upgrade(self.name, key=self.docstring["localization_key_name"]), description=desc_loc._upgrade( self.docstring["description"] or "-", key=self.docstring["localization_key_desc"] ), options=options or [], - dm_permission=dm_permission and not self._guild_only, + dm_permission=dm_permission, default_member_permissions=default_member_permissions, nsfw=nsfw, integration_types=integration_types, contexts=contexts, ) + self._apply_guild_only() + @property def root_parent(self) -> None: """``None``: This is for consistency with :class:`SubCommand` and :class:`SubCommandGroup`. @@ -755,7 +756,7 @@ def slash_command( *, name: LocalizedOptional = None, description: LocalizedOptional = None, - dm_permission: Optional[bool] = None, + dm_permission: Optional[bool] = None, # deprecated default_member_permissions: Optional[Union[Permissions, int]] = None, nsfw: Optional[bool] = None, integration_types: Optional[ApplicationIntegrationTypes] = None, @@ -810,6 +811,11 @@ def slash_command( dm_permission: :class:`bool` Whether this command can be used in DMs. Defaults to ``True``. + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. See :attr:`.ApplicationCommand.default_member_permissions` for details. diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index ba1d2d2bd2..d723518128 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -170,8 +170,9 @@ def filled_options(self) -> Dict[str, Any]: class GuildCommandInteraction(ApplicationCommandInteraction[ClientT]): """An :class:`ApplicationCommandInteraction` subclass, primarily meant for annotations. - This prevents the command from being invoked in DMs by automatically setting - :attr:`ApplicationCommand.dm_permission` to ``False`` for user/message commands and top-level slash commands. + This prevents the command from being invoked in DMs by automatically adjusting the + :attr:`~InteractionContextTypes.bot_dm` and :attr:`~InteractionContextTypes.private_channel` flags of + :attr:`ApplicationCommand.contexts` to ``False`` for user/message commands and top-level slash commands. Note that this does not apply to slash subcommands, subcommand groups, or autocomplete callbacks. @@ -182,7 +183,7 @@ class GuildCommandInteraction(ApplicationCommandInteraction[ClientT]): guild: Guild guild_id: int guild_locale: Locale - me: Member + me: Member # TODO: this might be inaccurate now class UserCommandInteraction(ApplicationCommandInteraction[ClientT]): diff --git a/docs/ext/commands/slash_commands.rst b/docs/ext/commands/slash_commands.rst index 603696b637..29fec4e0d2 100644 --- a/docs/ext/commands/slash_commands.rst +++ b/docs/ext/commands/slash_commands.rst @@ -717,3 +717,6 @@ Using this, you can specify if you want a certain slash command to work in DMs o ... This will make the ``config`` slash command invisible in DMs, while it will remain visible in guilds. + +.. + TODO From 4f504334c1e530913a9a4c0eb724a9bbc95f7ed1 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 30 Sep 2024 14:21:53 +0200 Subject: [PATCH 26/36] fix(docs): fix references in ext.commands --- disnake/ext/commands/ctx_menus_core.py | 8 ++++---- disnake/ext/commands/interaction_bot_base.py | 12 ++++++------ disnake/ext/commands/slash_core.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/disnake/ext/commands/ctx_menus_core.py b/disnake/ext/commands/ctx_menus_core.py index 2e094998b1..fff9292fdb 100644 --- a/disnake/ext/commands/ctx_menus_core.py +++ b/disnake/ext/commands/ctx_menus_core.py @@ -273,8 +273,8 @@ def user_command( Defaults to ``True``. .. deprecated:: 2.10 - Use :attr:`contexts` instead. - This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + Use ``contexts`` instead. + This is equivalent to the :attr:`.InteractionContextTypes.bot_dm` flag. default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. @@ -377,8 +377,8 @@ def message_command( Defaults to ``True``. .. deprecated:: 2.10 - Use :attr:`contexts` instead. - This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + Use ``contexts`` instead. + This is equivalent to the :attr:`.InteractionContextTypes.bot_dm` flag. default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index ce06c8b4a8..a9194fa1f7 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -524,8 +524,8 @@ def slash_command( Defaults to ``True``. .. deprecated:: 2.10 - Use :attr:`contexts` instead. - This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + Use ``contexts`` instead. + This is equivalent to the :attr:`.InteractionContextTypes.bot_dm` flag. default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. @@ -630,8 +630,8 @@ def user_command( Defaults to ``True``. .. deprecated:: 2.10 - Use :attr:`contexts` instead. - This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + Use ``contexts`` instead. + This is equivalent to the :attr:`.InteractionContextTypes.bot_dm` flag. default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. @@ -729,8 +729,8 @@ def message_command( Defaults to ``True``. .. deprecated:: 2.10 - Use :attr:`contexts` instead. - This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + Use ``contexts`` instead. + This is equivalent to the :attr:`.InteractionContextTypes.bot_dm` flag. default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index c794923ae5..bf8ee52cf3 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -813,8 +813,8 @@ def slash_command( Defaults to ``True``. .. deprecated:: 2.10 - Use :attr:`contexts` instead. - This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. + Use ``contexts`` instead. + This is equivalent to the :attr:`.InteractionContextTypes.bot_dm` flag. default_member_permissions: Optional[Union[:class:`.Permissions`, :class:`int`]] The default required permissions for this command. From 5cef0f9206fc330b161cf8108becd00ebc739669 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Mon, 30 Sep 2024 14:43:40 +0200 Subject: [PATCH 27/36] fix: avoid resync for old commands with default settings (i.e. those with `dm_permission=True, contexts=None`) --- disnake/app_commands.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index ff4ed96d9e..e961a60832 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -634,7 +634,6 @@ def __eq__(self, other) -> bool: ) or ( self.integration_types == other.integration_types - # TODO: `None` and (guild=True, bot_dm=True) are practically the same and self.contexts == other.contexts ) ) @@ -686,8 +685,10 @@ def _update_common(self, data: ApplicationCommandPayload) -> None: # deprecated, but kept until API stops returning this field self._default_permission = data.get("default_permission") is not False - # same deal, also deprecated - if (dm_permission := data.get("dm_permission")) is not None: + # same deal, also deprecated. + # Only apply if set to `False`, we want to keep `contexts` as-is if + # `dm_permission` is the default value (i.e. None/True) for syncing purposes + if (dm_permission := data.get("dm_permission")) is False: self._convert_dm_permission(dm_permission) From 34b97aed62f997bedaa8eefb2fd0c0dba09c48b0 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 1 Oct 2024 13:01:09 +0200 Subject: [PATCH 28/36] refactor: keep `dm_permission` and `contexts` completely separate --- disnake/app_commands.py | 96 +++++++++++++++++++++++-------- disnake/ext/commands/base_core.py | 12 +++- 2 files changed, 80 insertions(+), 28 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index e961a60832..f6a70c70b3 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -486,6 +486,15 @@ class ApplicationCommand(ABC): .. versionadded:: 2.5 + dm_permission: :class:`bool` + Whether this command can be used in DMs. + Defaults to ``True``. + + .. versionadded:: 2.5 + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -554,11 +563,9 @@ def __init__( # reset `default_permission` if set before self._default_permission: bool = True - # TODO: consider throwing if both `dm_permission` and `contexts` are provided; take `GuildCommandInteraction` into account as well. - - # `dm_permission` is deprecated; this turns it into `contexts.bot_dm`, which is equivalent. - # The API computes `dm_permission` based on `contexts` anyway (if set), so `contexts` is the - # source of truth here; it ignores `dm_permission` if `contexts` is set. + # TODO: throw if both dm_permission and contexts are set; also consider this for property setters + self._dm_permission: Optional[bool] = None + # use the property setter to emit a deprecation warning if dm_permission is not None: self.dm_permission = dm_permission @@ -592,25 +599,13 @@ def dm_permission(self) -> bool: Use :attr:`contexts` instead. This is equivalent to the :attr:`InteractionContextTypes.bot_dm` flag. """ - if self.contexts is not None: - return self.contexts.bot_dm - - return True + # a `None` value is equivalent to `True` here + return self._dm_permission is not False @dm_permission.setter @deprecated("contexts") def dm_permission(self, value: bool) -> None: - self._convert_dm_permission(value) - - # this is separate so we can call it internally while avoiding DeprecationWarnings - def _convert_dm_permission(self, value: bool, *, apply_private_channel: bool = False) -> None: - if self.contexts is None: - # this is the default if neither dm_permission nor contexts are set - self.contexts = InteractionContextTypes(guild=True, bot_dm=True) - - self.contexts.bot_dm = value - if apply_private_channel: - self.contexts.private_channel = value + self._dm_permission = value def __repr__(self) -> str: attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__) @@ -633,7 +628,8 @@ def __eq__(self, other) -> bool: for obj in (self, other) ) or ( - self.integration_types == other.integration_types + self.dm_permission == other.dm_permission + and self.integration_types == other.integration_types and self.contexts == other.contexts ) ) @@ -649,6 +645,8 @@ def to_dict(self) -> EditApplicationCommandPayload: if self._default_member_permissions is not None else None ), + # Discord will ignore this if `contexts` is also set + "dm_permission": self._dm_permission is not False, "default_permission": True, "nsfw": self.nsfw, } @@ -686,10 +684,7 @@ def _update_common(self, data: ApplicationCommandPayload) -> None: self._default_permission = data.get("default_permission") is not False # same deal, also deprecated. - # Only apply if set to `False`, we want to keep `contexts` as-is if - # `dm_permission` is the default value (i.e. None/True) for syncing purposes - if (dm_permission := data.get("dm_permission")) is False: - self._convert_dm_permission(dm_permission) + self._dm_permission = data.get("dm_permission") class UserCommand(ApplicationCommand): @@ -704,6 +699,15 @@ class UserCommand(ApplicationCommand): .. versionadded:: 2.5 + dm_permission: :class:`bool` + Whether this command can be used in DMs. + Defaults to ``True``. + + .. versionadded:: 2.5 + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -760,6 +764,14 @@ class APIUserCommand(UserCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 + dm_permission: :class:`bool` + Whether this command can be used in DMs. + + .. versionadded:: 2.5 + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. @@ -827,6 +839,15 @@ class MessageCommand(ApplicationCommand): .. versionadded:: 2.5 + dm_permission: :class:`bool` + Whether this command can be used in DMs. + Defaults to ``True``. + + .. versionadded:: 2.5 + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -883,6 +904,14 @@ class APIMessageCommand(MessageCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 + dm_permission: :class:`bool` + Whether this command can be used in DMs. + + .. versionadded:: 2.5 + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. @@ -957,6 +986,15 @@ class SlashCommand(ApplicationCommand): .. versionadded:: 2.5 + dm_permission: :class:`bool` + Whether this command can be used in DMs. + Defaults to ``True``. + + .. versionadded:: 2.5 + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -1097,6 +1135,14 @@ class APISlashCommand(SlashCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 + dm_permission: :class:`bool` + Whether this command can be used in DMs. + + .. versionadded:: 2.5 + + .. deprecated:: 2.10 + Use :attr:`contexts` instead. + nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 3824095601..81e99d11fe 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -144,6 +144,7 @@ def __init__(self, func: CommandCallback, *, name: Optional[str] = None, **kwarg if not isinstance(self.name, str): raise TypeError("Name of a command must be a string.") + # TODO if "default_permission" in kwargs: raise TypeError( "`default_permission` is deprecated and will always be set to `True`. " @@ -202,6 +203,8 @@ def _ensure_assignment_on_copy(self, other: AppCommandT) -> AppCommandT: ): other.body._default_member_permissions = self.body._default_member_permissions + # TODO: contexts? + try: other.on_error = self.on_error except AttributeError: @@ -230,10 +233,13 @@ def _update_copy(self: AppCommandT, kwargs: Dict[str, Any]) -> AppCommandT: return self.copy() def _apply_guild_only(self) -> None: - # If we have a `GuildCommandInteraction` annotation, - # turn it into `contexts.bot_dm = contexts.private_channel = False` + # If we have a `GuildCommandInteraction` annotation, set `contexts` accordingly. if self._guild_only: - self.body._convert_dm_permission(False, apply_private_channel=True) + if self.body.contexts is None: + self.body.contexts = InteractionContextTypes(guild=True) + else: + self.body.contexts.bot_dm = False + self.body.contexts.private_channel = False @property def dm_permission(self) -> bool: From 009f787666ab9bea44861514403c098362148a09 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 1 Oct 2024 13:31:35 +0200 Subject: [PATCH 29/36] fix: change `__eq__` to ignore `dm_permission` if `contexts` is set --- disnake/app_commands.py | 37 +++++++++++++++++++------------ disnake/ext/commands/base_core.py | 1 + 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index f6a70c70b3..279849724e 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -615,26 +615,35 @@ def __str__(self) -> str: return self.name def __eq__(self, other) -> bool: - return ( + if not ( self.type == other.type and self.name == other.name and self.name_localizations == other.name_localizations and self.nsfw == other.nsfw and self._default_member_permissions == other._default_member_permissions - # ignore global-only fields if comparing guild commands - and ( - any( - (isinstance(obj, _APIApplicationCommandMixin) and obj.guild_id) - for obj in (self, other) - ) - or ( - self.dm_permission == other.dm_permission - and self.integration_types == other.integration_types - and self.contexts == other.contexts - ) - ) and self._default_permission == other._default_permission - ) + ): + return False + + # ignore global-only fields if comparing guild commands + if not any( + (isinstance(obj, _APIApplicationCommandMixin) and obj.guild_id) for obj in (self, other) + ): + if self.integration_types != other.integration_types: + return False + + # `contexts` takes priority over `dm_permission`; + # ignore `dm_permission` if `contexts` is set, + # since the API returns both even when only `contexts` was provided + if self.contexts is not None or other.contexts is not None: + if self.contexts != other.contexts: + return False + else: + # this is a bit awkward; `None` is equivalent to `True` in this case + if (self._dm_permission is not False) != (other._dm_permission is not False): + return False + + return True def to_dict(self) -> EditApplicationCommandPayload: data: EditApplicationCommandPayload = { diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 81e99d11fe..11701a6540 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -238,6 +238,7 @@ def _apply_guild_only(self) -> None: if self.body.contexts is None: self.body.contexts = InteractionContextTypes(guild=True) else: + # TODO: copy? self.body.contexts.bot_dm = False self.body.contexts.private_channel = False From 83214038e0214a474fe5ac8f5d9db814d6a25e04 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 1 Oct 2024 13:31:47 +0200 Subject: [PATCH 30/36] fix: don't send `dm_permission` if `contexts` is set this isn't required, but done just to be sure, since the override behavior of the api isn't documented --- disnake/app_commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index 279849724e..9287f0151b 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -654,8 +654,6 @@ def to_dict(self) -> EditApplicationCommandPayload: if self._default_member_permissions is not None else None ), - # Discord will ignore this if `contexts` is also set - "dm_permission": self._dm_permission is not False, "default_permission": True, "nsfw": self.nsfw, } @@ -668,6 +666,10 @@ def to_dict(self) -> EditApplicationCommandPayload: contexts = self.contexts.values if self.contexts is not None else None data["contexts"] = contexts + # don't set `dm_permission` if `contexts` is set + if contexts is None: + data["dm_permission"] = self._dm_permission is not False + if (loc := self.name_localizations.data) is not None: data["name_localizations"] = loc From bef05ec68a6d76f7785dcdf010bc1561328cf11c Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 1 Oct 2024 14:02:34 +0200 Subject: [PATCH 31/36] feat: raise error if both parameters are set --- disnake/app_commands.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index 9287f0151b..3b51166f68 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -563,12 +563,17 @@ def __init__( # reset `default_permission` if set before self._default_permission: bool = True - # TODO: throw if both dm_permission and contexts are set; also consider this for property setters self._dm_permission: Optional[bool] = None - # use the property setter to emit a deprecation warning if dm_permission is not None: + # use the property setter to emit a deprecation warning self.dm_permission = dm_permission + # if both are provided, raise an exception + # (n.b. these can be assigned to later, in which case no exception will be raised. + # assume the user knows what they're doing, in that case) + if self.contexts is not None: + raise ValueError("Cannot use both `dm_permission` and `contexts` at the same time") + @property def default_member_permissions(self) -> Optional[Permissions]: """Optional[:class:`Permissions`]: The default required member permissions for this command. From eabb563374c1cda8c3a1a6ffd1a331867ec1781c Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 1 Oct 2024 18:03:33 +0200 Subject: [PATCH 32/36] feat: show `dm_permission` warning in user code (3.12+) --- disnake/app_commands.py | 15 ++++++++++----- disnake/utils.py | 19 ++++++++++++++++--- tests/test_utils.py | 24 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index 3b51166f68..feb4f98e24 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -20,7 +20,7 @@ from .flags import ApplicationIntegrationTypes, InteractionContextTypes from .i18n import Localized from .permissions import Permissions -from .utils import MISSING, _get_as_snowflake, _maybe_cast, deprecated +from .utils import MISSING, _get_as_snowflake, _maybe_cast, deprecated, warn_deprecated if TYPE_CHECKING: from typing_extensions import Self @@ -563,10 +563,15 @@ def __init__( # reset `default_permission` if set before self._default_permission: bool = True - self._dm_permission: Optional[bool] = None - if dm_permission is not None: - # use the property setter to emit a deprecation warning - self.dm_permission = dm_permission + self._dm_permission: Optional[bool] = dm_permission + if self._dm_permission is not None: + warn_deprecated( + "dm_permission is deprecated, use contexts instead.", + stacklevel=2, + # the call stack can have different depths, depending on how the + # user created the command, so we can't reliably set a fixed stacklevel + skip_internal_frames=True, + ) # if both are provided, raise an exception # (n.b. these can be assigned to later, in which case no exception will be raised. diff --git a/disnake/utils.py b/disnake/utils.py index 88fc4aeb29..55e0f52281 100644 --- a/disnake/utils.py +++ b/disnake/utils.py @@ -256,7 +256,9 @@ def decorator(overriden: T) -> T: return decorator -def deprecated(instead: Optional[str] = None) -> Callable[[Callable[P, T]], Callable[P, T]]: +def deprecated( + instead: Optional[str] = None, *, skip_internal_frames: bool = False +) -> Callable[[Callable[P, T]], Callable[P, T]]: def actual_decorator(func: Callable[P, T]) -> Callable[P, T]: @functools.wraps(func) def decorated(*args: P.args, **kwargs: P.kwargs) -> T: @@ -265,7 +267,7 @@ def decorated(*args: P.args, **kwargs: P.kwargs) -> T: else: msg = f"{func.__name__} is deprecated." - warn_deprecated(msg, stacklevel=2) + warn_deprecated(msg, stacklevel=2, skip_internal_frames=skip_internal_frames) return func(*args, **kwargs) return decorated @@ -273,7 +275,18 @@ def decorated(*args: P.args, **kwargs: P.kwargs) -> T: return actual_decorator -def warn_deprecated(*args: Any, stacklevel: int = 1, **kwargs: Any) -> None: +_root_module_path = os.path.join(os.path.dirname(__file__), "") # add trailing slash + + +def warn_deprecated( + *args: Any, stacklevel: int = 1, skip_internal_frames: bool = False, **kwargs: Any +) -> None: + # NOTE: skip_file_prefixes was added in 3.12; in older versions, + # we'll just have to live with the warning location possibly being wrong + if sys.version_info >= (3, 12) and skip_internal_frames: + kwargs["skip_file_prefixes"] = (_root_module_path,) + stacklevel = 1 # reset stacklevel, assume we just want the first frame outside library code + old_filters = warnings.filters[:] try: warnings.simplefilter("always", DeprecationWarning) diff --git a/tests/test_utils.py b/tests/test_utils.py index 96c5c4bb5e..75e3944151 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -94,6 +94,30 @@ def stuff(num: int) -> int: mock_warn.assert_called_once_with(msg, stacklevel=3, category=DeprecationWarning) +@mock.patch.object(utils, "_root_module_path", os.path.dirname(__file__)) +@pytest.mark.xfail( + sys.version_info < (3, 12), + raises=AssertionError, + strict=True, + reason="requires 3.12 functionality", +) +def test_deprecated_skip() -> None: + def func(n: int) -> None: + if n == 0: + utils.warn_deprecated("test", skip_internal_frames=True) + else: + func(n - 1) + + with warnings.catch_warnings(record=True) as result: + # show a warning a couple frames deep + func(10) + + # if we successfully skipped all frames in the current module, + # we should end up in the mock decorator's frame + assert len(result) == 1 + assert result[0].filename == mock.__file__ + + @pytest.mark.parametrize( ("params", "expected"), [ From c4885c44bfad4cecb96ddcbfde4560b1000a4e1f Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 1 Oct 2024 18:12:21 +0200 Subject: [PATCH 33/36] fix: update `default_permission` error text --- disnake/ext/commands/base_core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 11701a6540..2b4f4c0857 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -144,11 +144,10 @@ def __init__(self, func: CommandCallback, *, name: Optional[str] = None, **kwarg if not isinstance(self.name, str): raise TypeError("Name of a command must be a string.") - # TODO if "default_permission" in kwargs: raise TypeError( "`default_permission` is deprecated and will always be set to `True`. " - "See `default_member_permissions` and `dm_permission` instead." + "See `default_member_permissions` and `contexts` instead." ) try: From fbd95c8528966eb16e73033342bc32ba8fe81dd4 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 1 Oct 2024 18:12:37 +0200 Subject: [PATCH 34/36] fix: make `GuildCommandInteraction` always overwrite contexts --- disnake/ext/commands/base_core.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index 2b4f4c0857..f3da1165c8 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -234,12 +234,9 @@ def _update_copy(self: AppCommandT, kwargs: Dict[str, Any]) -> AppCommandT: def _apply_guild_only(self) -> None: # If we have a `GuildCommandInteraction` annotation, set `contexts` accordingly. if self._guild_only: - if self.body.contexts is None: - self.body.contexts = InteractionContextTypes(guild=True) - else: - # TODO: copy? - self.body.contexts.bot_dm = False - self.body.contexts.private_channel = False + # n.b. this overwrites any user-specified `contexts` parameter, + # which is fine at least as long as no new contexts are added to the API + self.body.contexts = InteractionContextTypes(guild=True) @property def dm_permission(self) -> bool: From 3ca6790e7bd6cbfc62408714007799cb10a1177d Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 1 Oct 2024 18:23:04 +0200 Subject: [PATCH 35/36] feat: provide (hopefully) helpful error message when trying to set new fields on subcommands --- disnake/ext/commands/slash_core.py | 36 ++++++++++++++++-------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index bf8ee52cf3..c19280061b 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -98,6 +98,23 @@ async def _call_autocompleter( return choices +_INVALID_SUB_KWARGS = frozenset( + {"dm_permission", "default_member_permissions", "integration_types", "contexts"} +) + + +# this is just a helpful message for users trying to set specific +# top-level-only fields on subcommands or groups +def _check_invalid_sub_kwargs(func: CommandCallback, kwargs: Dict[str, Any]) -> None: + invalid_keys = kwargs.keys() & _INVALID_SUB_KWARGS + if hasattr(func, "__default_member_permissions__"): + invalid_keys.add("default_member_permissions") + + if invalid_keys: + msg = f"Cannot set {utils.humanize_list(list(invalid_keys), 'or')} on subcommands or subcommand groups" + raise TypeError(msg) + + class SubCommandGroup(InvokableApplicationCommand): """A class that implements the protocol for a bot slash command group. @@ -157,15 +174,7 @@ def __init__( ) self.qualified_name: str = f"{parent.qualified_name} {self.name}" - # TODO - if ( - "dm_permission" in kwargs - or "default_member_permissions" in kwargs - or hasattr(func, "__default_member_permissions__") - ): - raise TypeError( - "Cannot set `default_member_permissions` or `dm_permission` on subcommand groups" - ) + _check_invalid_sub_kwargs(func, kwargs) @property def root_parent(self) -> InvokableSlashCommand: @@ -298,14 +307,7 @@ def __init__( ) self.qualified_name = f"{parent.qualified_name} {self.name}" - if ( - "dm_permission" in kwargs - or "default_member_permissions" in kwargs - or hasattr(func, "__default_member_permissions__") - ): - raise TypeError( - "Cannot set `default_member_permissions` or `dm_permission` on subcommands" - ) + _check_invalid_sub_kwargs(func, kwargs) @property def root_parent(self) -> InvokableSlashCommand: From ad2fb7c6602623fe0a67ec066dcc96d1e60491fa Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 1 Oct 2024 19:07:15 +0200 Subject: [PATCH 36/36] fix(docs): remove duplicate `dm_permission` documentation --- disnake/app_commands.py | 60 ----------------------------------------- 1 file changed, 60 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index feb4f98e24..b767c61a0f 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -486,15 +486,6 @@ class ApplicationCommand(ABC): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - Defaults to ``True``. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -720,15 +711,6 @@ class UserCommand(ApplicationCommand): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - Defaults to ``True``. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -785,14 +767,6 @@ class APIUserCommand(UserCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. @@ -860,15 +834,6 @@ class MessageCommand(ApplicationCommand): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - Defaults to ``True``. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -925,14 +890,6 @@ class APIMessageCommand(MessageCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. @@ -1007,15 +964,6 @@ class SlashCommand(ApplicationCommand): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - Defaults to ``True``. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `. Defaults to ``False``. @@ -1156,14 +1104,6 @@ class APISlashCommand(SlashCommand, _APIApplicationCommandMixin): .. versionadded:: 2.5 - dm_permission: :class:`bool` - Whether this command can be used in DMs. - - .. versionadded:: 2.5 - - .. deprecated:: 2.10 - Use :attr:`contexts` instead. - nsfw: :class:`bool` Whether this command is :ddocs:`age-restricted `.