diff --git a/changes/1455.breaking.md b/changes/1455.breaking.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/changes/1455.deprecation.md b/changes/1455.deprecation.md new file mode 100644 index 0000000000..7c84d5245e --- /dev/null +++ b/changes/1455.deprecation.md @@ -0,0 +1,4 @@ +Deprecate selects v1 functionality: + - `ComponentType.SELECT_MENU` -> `ComponentType.TEXT_SELECT_MENU` + - Not passing `MessageActionRowBuilder.add_select_menu`'s `type` argument explicitly. + - `InteractionChannel` and `ResolvedOptionData` moved from `hikari.interactions.command_interactions` to `hikari.interactions.base_interactions`. diff --git a/changes/1455.feature.md b/changes/1455.feature.md new file mode 100644 index 0000000000..e5c9dbbc9b --- /dev/null +++ b/changes/1455.feature.md @@ -0,0 +1 @@ +Add selects v2 components. diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 27c8851453..825207f7b2 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -1460,7 +1460,7 @@ def add_to_menu(self) -> _SelectMenuBuilderT: class SelectMenuBuilder(ComponentBuilder, abc.ABC, typing.Generic[_ContainerT]): - """Builder class for select menu options.""" + """Builder class for a select menu.""" __slots__: typing.Sequence[str] = () @@ -1474,11 +1474,6 @@ def custom_id(self) -> str: def is_disabled(self) -> bool: """Whether the select menu should be marked as disabled.""" - @property - @abc.abstractmethod - def options(self: _SelectMenuBuilderT) -> typing.Sequence[SelectOptionBuilder[_SelectMenuBuilderT]]: - """Sequence of the options set for this select menu.""" - @property @abc.abstractmethod def placeholder(self) -> undefined.UndefinedOr[str]: @@ -1504,27 +1499,6 @@ def max_values(self) -> int: less than or equal to 25. """ - @abc.abstractmethod - def add_option(self: _SelectMenuBuilderT, label: str, value: str, /) -> SelectOptionBuilder[_SelectMenuBuilderT]: - """Add an option to this menu. - - .. note:: - Setup should be finalised by calling `add_to_menu` in the builder - returned. - - Parameters - ---------- - label : str - The user-facing name of this option, max 100 characters. - value : str - The developer defined value of this option, max 100 characters. - - Returns - ------- - SelectOptionBuilder[SelectMenuBuilder] - Option builder object. - """ - @abc.abstractmethod def set_is_disabled(self: _T, state: bool, /) -> _T: """Set whether this option is disabled. @@ -1607,6 +1581,64 @@ def add_to_container(self) -> _ContainerT: """ +class TextSelectMenuBuilder(SelectMenuBuilder[_ContainerT], abc.ABC, typing.Generic[_ContainerT]): + """Builder class for a text select menu.""" + + __slots__: typing.Sequence[str] = () + + @property + @abc.abstractmethod + def options(self: _SelectMenuBuilderT) -> typing.Sequence[SelectOptionBuilder[_SelectMenuBuilderT]]: + """Sequence of the options set for this select menu.""" + + @abc.abstractmethod + def add_option(self: _SelectMenuBuilderT, label: str, value: str, /) -> SelectOptionBuilder[_SelectMenuBuilderT]: + """Add an option to this menu. + + .. note:: + Setup should be finalised by calling `add_to_menu` in the builder + returned. + + Parameters + ---------- + label : str + The user-facing name of this option, max 100 characters. + value : str + The developer defined value of this option, max 100 characters. + + Returns + ------- + SelectOptionBuilder[SelectMenuBuilder] + Option builder object. + """ + + +class ChannelSelectMenuBuilder(SelectMenuBuilder[_ContainerT], abc.ABC, typing.Generic[_ContainerT]): + """Builder class for a channel select menu.""" + + __slots__: typing.Sequence[str] = () + + @property + @abc.abstractmethod + def channel_types(self) -> typing.Sequence[channels.ChannelType]: + """The channel types that can be selected in this menu.""" + + @abc.abstractmethod + def set_channel_types(self: _T, value: typing.Sequence[channels.ChannelType], /) -> _T: + """Set the valid channel types for this menu. + + Parameters + ---------- + value : typing.Sequence[hikari.channels.ChannelType] + The valid channel types for this menu. + + Returns + ------- + SelectMenuBuilder + The builder object to enable chained calls. + """ + + class TextInputBuilder(ComponentBuilder, abc.ABC, typing.Generic[_ContainerT]): """Builder class for text inputs components.""" @@ -1868,12 +1900,63 @@ def add_button( component. """ + @typing.overload # Deprecated overload + @abc.abstractmethod + def add_select_menu( + self: _T, + custom_id: str, + /, + ) -> TextSelectMenuBuilder[_T]: + ... + + @typing.overload + @abc.abstractmethod + def add_select_menu( + self: _T, + type_: typing.Literal[components_.ComponentType.TEXT_SELECT_MENU, 3], + custom_id: str, + /, + ) -> TextSelectMenuBuilder[_T]: + ... + + @typing.overload + @abc.abstractmethod + def add_select_menu( + self: _T, + type_: typing.Literal[components_.ComponentType.CHANNEL_SELECT_MENU, 8], + custom_id: str, + /, + ) -> ChannelSelectMenuBuilder[_T]: + ... + + @typing.overload @abc.abstractmethod - def add_select_menu(self: _T, custom_id: str, /) -> SelectMenuBuilder[_T]: + def add_select_menu( + self: _T, + type_: typing.Union[components_.ComponentType, int], + custom_id: str, + /, + ) -> SelectMenuBuilder[_T]: + ... + + @abc.abstractmethod + def add_select_menu( + self: _T, + type_: typing.Union[components_.ComponentType, int, str], + # These have default during the deprecation period for backwards compatibility, as custom_id + # used to come first + custom_id: str = "", + /, + ) -> SelectMenuBuilder[_T]: """Add a select menu component to this action row builder. + .. deprecated:: 2.0.0.dev116 + `type_` now comes as a positional-only argument before `custom_id`. + Parameters ---------- + type_ : typing.Union[hikari.components.ComponentType, int] + The type for the select menu. custom_id : str A developer-defined custom identifier used to identify which menu triggered component interactions. @@ -1884,6 +1967,11 @@ def add_select_menu(self: _T, custom_id: str, /) -> SelectMenuBuilder[_T]: Select menu builder object. `SelectMenuBuilder.add_to_container` should be called to finalise the component. + + Raises + ------ + ValueError + If an invalid select menu type is passed. """ diff --git a/hikari/components.py b/hikari/components.py index b6aaeb55ed..568bdec53f 100644 --- a/hikari/components.py +++ b/hikari/components.py @@ -46,6 +46,7 @@ import attr +from hikari import channels from hikari import emojis from hikari.internal import enums @@ -73,14 +74,17 @@ class ComponentType(int, enums.Enum): as `ComponentType.ACTION_ROW`. """ - SELECT_MENU = 3 - """A select menu component. + TEXT_SELECT_MENU = 3 + """A text select component. .. note:: This cannot be top-level and must be within a container component such as `ComponentType.ACTION_ROW`. """ + SELECT_MENU = enums.deprecated(TEXT_SELECT_MENU, removal_version="2.0.0.dev118") + """Deprecated alias for `TEXT_MENU_SELECT`.""" + TEXT_INPUT = 4 """A text input component. @@ -92,6 +96,38 @@ class ComponentType(int, enums.Enum): as `ComponentType.ACTION_ROW`. """ + USER_SELECT_MENU = 5 + """A user select component. + + .. note:: + This cannot be top-level and must be within a container component such + as `ComponentType.ACTION_ROW`. + """ + + ROLE_SELECT_MENU = 6 + """A role select component. + + .. note:: + This cannot be top-level and must be within a container component such + as `ComponentType.ACTION_ROW`. + """ + + MENTIONABLE_SELECT_MENU = 7 + """A mentionable (users and roles) select component. + + .. note:: + This cannot be top-level and must be within a container component such + as `ComponentType.ACTION_ROW`. + """ + + CHANNEL_SELECT_MENU = 8 + """A channel select component. + + .. note:: + This cannot be top-level and must be within a container component such + as `ComponentType.ACTION_ROW`. + """ + @typing.final class ButtonStyle(int, enums.Enum): @@ -230,9 +266,6 @@ class SelectMenuComponent(PartialComponent): custom_id: str = attr.field(hash=True) """Developer defined identifier for this menu (will be <= 100 characters).""" - options: typing.Sequence[SelectMenuOption] = attr.field(eq=False) - """Sequence of up to 25 of the options set for this menu.""" - placeholder: typing.Optional[str] = attr.field(eq=False) """Custom placeholder text shown if nothing is selected, max 100 characters.""" @@ -254,6 +287,22 @@ class SelectMenuComponent(PartialComponent): """Whether the select menu is disabled.""" +@attr.define(kw_only=True, weakref_slot=False) +class TextSelectMenuComponent(SelectMenuComponent): + """Represents a text select menu component.""" + + options: typing.Sequence[SelectMenuOption] = attr.field(eq=False) + """Sequence of up to 25 of the options set for this menu.""" + + +@attr.define(kw_only=True, weakref_slot=False) +class ChannelSelectMenuComponent(SelectMenuComponent): + """Represents a channel select menu component.""" + + channel_types: typing.Sequence[typing.Union[int, channels.ChannelType]] = attr.field(eq=False) + """The valid channel types for this menu.""" + + @attr.define(kw_only=True, weakref_slot=False) class TextInputComponent(PartialComponent): """Represents a text input component.""" @@ -265,6 +314,49 @@ class TextInputComponent(PartialComponent): """Value provided for this text input.""" +SelectMenuTypesT = typing.Union[ + typing.Literal[ComponentType.TEXT_SELECT_MENU], + typing.Literal[3], + typing.Literal[ComponentType.USER_SELECT_MENU], + typing.Literal[5], + typing.Literal[ComponentType.ROLE_SELECT_MENU], + typing.Literal[6], + typing.Literal[ComponentType.MENTIONABLE_SELECT_MENU], + typing.Literal[7], + typing.Literal[ComponentType.CHANNEL_SELECT_MENU], + typing.Literal[8], +] +"""Type hints of the `ComponentType` values which are valid for select menus. + +The following values are valid for this: + +* `ComponentType.TEXT_SELECT_MENU`/`3` +* `ComponentType.USER_SELECT_MENU`/`5` +* `ComponentType.ROLE_SELECT_MENU`/`6` +* `ComponentType.MENTIONABLE_SELECT_MENU`/`7` +* `ComponentType.CHANNEL_SELECT_MENU`/`8` +""" + +SelectMenuTypes: typing.AbstractSet[SelectMenuTypesT] = frozenset( + ( + ComponentType.TEXT_SELECT_MENU, + ComponentType.USER_SELECT_MENU, + ComponentType.ROLE_SELECT_MENU, + ComponentType.MENTIONABLE_SELECT_MENU, + ComponentType.CHANNEL_SELECT_MENU, + ) +) +"""Set of the `ComponentType` values which are valid for select menus. + +The following values are included in this: + +* `ComponentType.TEXT_SELECT_MENU` +* `ComponentType.USER_SELECT_MENU` +* `ComponentType.ROLE_SELECT_MENU` +* `ComponentType.MENTIONABLE_SELECT_MENU` +* `ComponentType.CHANNEL_SELECT_MENU` +""" + InteractiveButtonTypesT = typing.Union[ typing.Literal[ButtonStyle.PRIMARY], typing.Literal[1], diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index b5c5c332f1..fb753048bd 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -507,7 +507,11 @@ def __init__(self, app: traits.RESTAware) -> None: int, typing.Callable[[data_binding.JSONObject], component_models.MessageComponentTypesT] ] = { component_models.ComponentType.BUTTON: self._deserialize_button, - component_models.ComponentType.SELECT_MENU: self._deserialize_select_menu, + component_models.ComponentType.TEXT_SELECT_MENU: self._deserialize_text_select_menu, + component_models.ComponentType.USER_SELECT_MENU: self._deserialize_select_menu, + component_models.ComponentType.ROLE_SELECT_MENU: self._deserialize_select_menu, + component_models.ComponentType.MENTIONABLE_SELECT_MENU: self._deserialize_select_menu, + component_models.ComponentType.CHANNEL_SELECT_MENU: self._deserialize_channel_select_menu, } self._modal_component_type_mapping: typing.Dict[ int, typing.Callable[[data_binding.JSONObject], component_models.ModalComponentTypesT] @@ -2448,12 +2452,12 @@ def _deserialize_resolved_option_data( payload: data_binding.JSONObject, *, guild_id: typing.Optional[snowflakes.Snowflake] = None, - ) -> command_interactions.ResolvedOptionData: - channels: typing.Dict[snowflakes.Snowflake, command_interactions.InteractionChannel] = {} + ) -> base_interactions.ResolvedOptionData: + channels: typing.Dict[snowflakes.Snowflake, base_interactions.InteractionChannel] = {} if raw_channels := payload.get("channels"): for channel_payload in raw_channels.values(): channel_id = snowflakes.Snowflake(channel_payload["id"]) - channels[channel_id] = command_interactions.InteractionChannel( + channels[channel_id] = base_interactions.InteractionChannel( app=self._app, id=channel_id, type=channel_models.ChannelType(channel_payload["type"]), @@ -2496,7 +2500,7 @@ def _deserialize_resolved_option_data( else: attachments = {} - return command_interactions.ResolvedOptionData( + return base_interactions.ResolvedOptionData( attachments=attachments, channels=channels, members=members, @@ -2529,7 +2533,7 @@ def deserialize_command_interaction( member = None user = self.deserialize_user(payload["user"]) - resolved: typing.Optional[command_interactions.ResolvedOptionData] = None + resolved: typing.Optional[base_interactions.ResolvedOptionData] = None if resolved_payload := data_payload.get("resolved"): resolved = self._deserialize_resolved_option_data(resolved_payload, guild_id=guild_id) @@ -2706,6 +2710,10 @@ def deserialize_component_interaction( member = None user = self.deserialize_user(payload["user"]) + resolved: typing.Optional[base_interactions.ResolvedOptionData] = None + if resolved_payload := data_payload.get("resolved"): + resolved = self._deserialize_resolved_option_data(resolved_payload, guild_id=guild_id) + app_perms = payload.get("app_permissions") return component_interactions.ComponentInteraction( app=self._app, @@ -2718,6 +2726,7 @@ def deserialize_component_interaction( user=user, token=payload["token"], values=data_payload.get("values") or (), + resolved=resolved, version=payload["version"], custom_id=data_payload["custom_id"], component_type=component_models.ComponentType(data_payload["component_type"]), @@ -2843,23 +2852,36 @@ def _deserialize_button(self, payload: data_binding.JSONObject) -> component_mod ) def _deserialize_select_menu(self, payload: data_binding.JSONObject) -> component_models.SelectMenuComponent: + return component_models.SelectMenuComponent( + type=component_models.ComponentType(payload["type"]), + custom_id=payload["custom_id"], + placeholder=payload.get("placeholder"), + min_values=payload.get("min_values", 1), + max_values=payload.get("max_values", 1), + is_disabled=payload.get("disabled", False), + ) + + def _deserialize_text_select_menu( + self, payload: data_binding.JSONObject + ) -> component_models.TextSelectMenuComponent: options: typing.List[component_models.SelectMenuOption] = [] - for option_payload in payload["options"]: - emoji = None - if emoji_payload := option_payload.get("emoji"): - emoji = self.deserialize_emoji(emoji_payload) - - options.append( - component_models.SelectMenuOption( - label=option_payload["label"], - value=option_payload["value"], - description=option_payload.get("description"), - emoji=emoji, - is_default=option_payload.get("default", False), + if "options" in payload: + for option_payload in payload["options"]: + emoji = None + if emoji_payload := option_payload.get("emoji"): + emoji = self.deserialize_emoji(emoji_payload) + + options.append( + component_models.SelectMenuOption( + label=option_payload["label"], + value=option_payload["value"], + description=option_payload.get("description"), + emoji=emoji, + is_default=option_payload.get("default", False), + ) ) - ) - return component_models.SelectMenuComponent( + return component_models.TextSelectMenuComponent( type=component_models.ComponentType(payload["type"]), custom_id=payload["custom_id"], options=options, @@ -2869,6 +2891,24 @@ def _deserialize_select_menu(self, payload: data_binding.JSONObject) -> componen is_disabled=payload.get("disabled", False), ) + def _deserialize_channel_select_menu( + self, payload: data_binding.JSONObject + ) -> component_models.ChannelSelectMenuComponent: + channel_types: typing.List[typing.Union[int, channel_models.ChannelType]] = [] + if "channel_types" in payload: + for channel_type in payload["channel_types"]: + channel_types.append(channel_models.ChannelType(channel_type)) + + return component_models.ChannelSelectMenuComponent( + type=component_models.ComponentType(payload["type"]), + custom_id=payload["custom_id"], + channel_types=channel_types, + placeholder=payload.get("placeholder"), + min_values=payload.get("min_values", 1), + max_values=payload.get("max_values", 1), + is_disabled=payload.get("disabled", False), + ) + def _deserialize_text_input(self, payload: data_binding.JSONObject) -> component_models.TextInputComponent: return component_models.TextInputComponent( type=component_models.ComponentType(payload["type"]), diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 92175a2b76..e03b22b100 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -64,6 +64,7 @@ from hikari.interactions import base_interactions from hikari.internal import attr_extensions from hikari.internal import data_binding +from hikari.internal import deprecation from hikari.internal import mentions from hikari.internal import routes from hikari.internal import time @@ -98,6 +99,10 @@ _ButtonBuilderT = typing.TypeVar("_ButtonBuilderT", bound="_ButtonBuilder[typing.Any]") _SelectOptionBuilderT = typing.TypeVar("_SelectOptionBuilderT", bound="_SelectOptionBuilder[typing.Any]") _SelectMenuBuilderT = typing.TypeVar("_SelectMenuBuilderT", bound="SelectMenuBuilder[typing.Any]") + _TextSelectMenuBuilderT = typing.TypeVar("_TextSelectMenuBuilderT", bound="TextSelectMenuBuilder[typing.Any]") + _ChannelSelectMenuBuilderT = typing.TypeVar( + "_ChannelSelectMenuBuilderT", bound="ChannelSelectMenuBuilder[typing.Any]" + ) _TextInputBuilderT = typing.TypeVar("_TextInputBuilderT", bound="TextInputBuilder[typing.Any]") class _RequestCallSig(typing.Protocol): @@ -1562,10 +1567,10 @@ def custom_id(self) -> str: @attr_extensions.with_copy @attr.define(kw_only=True, weakref_slot=False) -class _SelectOptionBuilder(special_endpoints.SelectOptionBuilder["_SelectMenuBuilderT"]): +class _SelectOptionBuilder(special_endpoints.SelectOptionBuilder["_TextSelectMenuBuilderT"]): """Builder class for select menu options.""" - _menu: _SelectMenuBuilderT = attr.field(alias="menu") + _menu: _TextSelectMenuBuilderT = attr.field(alias="menu") _label: str = attr.field(alias="label") _value: str = attr.field(alias="value") _description: undefined.UndefinedOr[str] = attr.field(alias="description", default=undefined.UNDEFINED) @@ -1613,7 +1618,7 @@ def set_is_default(self: _SelectOptionBuilderT, state: bool, /) -> _SelectOption self._is_default = state return self - def add_to_menu(self) -> _SelectMenuBuilderT: + def add_to_menu(self) -> _TextSelectMenuBuilderT: self._menu.add_raw_option(self) return self._menu @@ -1640,9 +1645,8 @@ class SelectMenuBuilder(special_endpoints.SelectMenuBuilder[_ContainerProtoT]): """Builder class for select menus.""" _container: _ContainerProtoT = attr.field(alias="container") + _type: typing.Union[component_models.ComponentType, int] = attr.field(alias="type") _custom_id: str = attr.field(alias="custom_id") - # Any has to be used here as we can't access Self type in this context - _options: typing.List[special_endpoints.SelectOptionBuilder[typing.Any]] = attr.field(alias="options", factory=list) _placeholder: undefined.UndefinedOr[str] = attr.field(alias="placeholder", default=undefined.UNDEFINED) _min_values: int = attr.field(alias="min_values", default=0) _max_values: int = attr.field(alias="max_values", default=1) @@ -1656,12 +1660,6 @@ def custom_id(self) -> str: def is_disabled(self) -> bool: return self._is_disabled - @property - def options( - self: _SelectMenuBuilderT, - ) -> typing.Sequence[special_endpoints.SelectOptionBuilder[_SelectMenuBuilderT]]: - return self._options.copy() - @property def placeholder(self) -> undefined.UndefinedOr[str]: return self._placeholder @@ -1674,17 +1672,6 @@ def min_values(self) -> int: def max_values(self) -> int: return self._max_values - def add_option( - self: _SelectMenuBuilderT, label: str, value: str, / - ) -> special_endpoints.SelectOptionBuilder[_SelectMenuBuilderT]: - return _SelectOptionBuilder(menu=self, label=label, value=value) - - def add_raw_option( - self: _SelectMenuBuilderT, option: special_endpoints.SelectOptionBuilder[_SelectMenuBuilderT], / - ) -> _SelectMenuBuilderT: - self._options.append(option) - return self - def set_is_disabled(self: _SelectMenuBuilderT, state: bool, /) -> _SelectMenuBuilderT: self._is_disabled = state return self @@ -1708,9 +1695,8 @@ def add_to_container(self) -> _ContainerProtoT: def build(self) -> typing.MutableMapping[str, typing.Any]: data = data_binding.JSONObjectBuilder() - data["type"] = component_models.ComponentType.SELECT_MENU + data["type"] = self._type data["custom_id"] = self._custom_id - data["options"] = [option.build() for option in self._options] data.put("placeholder", self._placeholder) data.put("min_values", self._min_values) data.put("max_values", self._max_values) @@ -1718,6 +1704,68 @@ def build(self) -> typing.MutableMapping[str, typing.Any]: return data +@attr_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +class TextSelectMenuBuilder( + SelectMenuBuilder[_ContainerProtoT], special_endpoints.TextSelectMenuBuilder[_ContainerProtoT] +): + """Builder class for text select menus.""" + + _type: typing.Union[component_models.ComponentType, int] = component_models.ComponentType.TEXT_SELECT_MENU + # Any has to be used here as we can't access Self type in this context + _options: typing.List[special_endpoints.SelectOptionBuilder[typing.Any]] = attr.field(alias="options", factory=list) + + @property + def options( + self: _TextSelectMenuBuilderT, + ) -> typing.Sequence[special_endpoints.SelectOptionBuilder[_TextSelectMenuBuilderT]]: + return self._options.copy() + + def add_option( + self: _TextSelectMenuBuilderT, label: str, value: str, / + ) -> special_endpoints.SelectOptionBuilder[_TextSelectMenuBuilderT]: + return _SelectOptionBuilder(menu=self, label=label, value=value) + + def add_raw_option( + self: _TextSelectMenuBuilderT, option: special_endpoints.SelectOptionBuilder[_TextSelectMenuBuilderT], / + ) -> _TextSelectMenuBuilderT: + self._options.append(option) + return self + + def build(self) -> typing.MutableMapping[str, typing.Any]: + data = super().build() + + data["options"] = [option.build() for option in self._options] + return data + + +@attr_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +class ChannelSelectMenuBuilder( + SelectMenuBuilder[_ContainerProtoT], special_endpoints.ChannelSelectMenuBuilder[_ContainerProtoT] +): + """Builder class for channel select menus.""" + + _channel_types: typing.Sequence[channels.ChannelType] = attr.field(alias="channel_types", factory=list) + _type: typing.Union[component_models.ComponentType, int] = component_models.ComponentType.CHANNEL_SELECT_MENU + + @property + def channel_types(self) -> typing.Sequence[channels.ChannelType]: + return self._channel_types + + def set_channel_types( + self: _ChannelSelectMenuBuilderT, value: typing.Sequence[channels.ChannelType], / + ) -> _ChannelSelectMenuBuilderT: + self._channel_types = value + return self + + def build(self) -> typing.MutableMapping[str, typing.Any]: + data = super().build() + + data["channel_types"] = self._channel_types + return data + + @attr_extensions.with_copy @attr.define(kw_only=True, weakref_slot=False) class TextInputBuilder(special_endpoints.TextInputBuilder[_ContainerProtoT]): @@ -1889,11 +1937,75 @@ def add_button( return LinkButtonBuilder(container=self, style=style, url=url_or_custom_id) + @typing.overload # Deprecated overload + def add_select_menu( + self: _MessageActionRowBuilderT, + custom_id: str, + /, + ) -> special_endpoints.TextSelectMenuBuilder[_MessageActionRowBuilderT]: + ... + + @typing.overload + def add_select_menu( + self: _MessageActionRowBuilderT, + type_: typing.Literal[component_models.ComponentType.TEXT_SELECT_MENU, 3], + custom_id: str, + /, + ) -> special_endpoints.TextSelectMenuBuilder[_MessageActionRowBuilderT]: + ... + + @typing.overload + def add_select_menu( + self: _MessageActionRowBuilderT, + type_: typing.Literal[component_models.ComponentType.CHANNEL_SELECT_MENU, 8], + custom_id: str, + /, + ) -> special_endpoints.ChannelSelectMenuBuilder[_MessageActionRowBuilderT]: + ... + + @typing.overload + def add_select_menu( + self: _MessageActionRowBuilderT, + type_: typing.Union[component_models.ComponentType, int], + custom_id: str, + /, + ) -> special_endpoints.SelectMenuBuilder[_MessageActionRowBuilderT]: + ... + def add_select_menu( - self: _MessageActionRowBuilderT, custom_id: str, / + self: _MessageActionRowBuilderT, + type_: typing.Union[component_models.ComponentType, int, str], + # These have default during the deprecation period for backwards compatibility, as custom_id + # used to come first + custom_id: str = "", + /, ) -> special_endpoints.SelectMenuBuilder[_MessageActionRowBuilderT]: - self._assert_can_add_type(component_models.ComponentType.SELECT_MENU) - return SelectMenuBuilder(container=self, custom_id=custom_id) + # custom_id used to come first, so just switch them around if only of them is passed + if type_ and not custom_id: + custom_id = str(type_) + + deprecation.warn_deprecated( + "not passing 'type' explicitly", + removal_version="2.0.0.dev118", + additional_info="Please set the type by passing 'type' explicitly", + quote=False, + ) + type_ = component_models.ComponentType.TEXT_SELECT_MENU + + # A little guard during the deprecation period to stop mypy from complaining + type_ = component_models.ComponentType(type_) + + if type_ not in component_models.SelectMenuTypes: + raise ValueError(f"{type_!r} is an invalid type option") + + self._assert_can_add_type(type_) + + if type_ == component_models.ComponentType.TEXT_SELECT_MENU: + return TextSelectMenuBuilder(container=self, custom_id=custom_id) + if type_ == component_models.ComponentType.CHANNEL_SELECT_MENU: + return ChannelSelectMenuBuilder(container=self, custom_id=custom_id) + + return SelectMenuBuilder(container=self, type=type_, custom_id=custom_id) def build(self) -> typing.MutableMapping[str, typing.Any]: return { diff --git a/hikari/interactions/base_interactions.py b/hikari/interactions/base_interactions.py index d23eaf76b4..bc272138d1 100644 --- a/hikari/interactions/base_interactions.py +++ b/hikari/interactions/base_interactions.py @@ -27,6 +27,8 @@ "DEFERRED_RESPONSE_TYPES", "DeferredResponseTypesT", "InteractionMember", + "InteractionChannel", + "ResolvedOptionData", "InteractionType", "MessageResponseMixin", "MESSAGE_RESPONSE_TYPES", @@ -40,6 +42,7 @@ import attr +from hikari import channels from hikari import guilds from hikari import snowflakes from hikari import undefined @@ -614,3 +617,36 @@ class InteractionMember(guilds.Member): permissions: permissions_.Permissions = attr.field(eq=False, hash=False, repr=False) """Permissions the member has in the current channel.""" + + +@attr_extensions.with_copy +@attr.define(hash=True, kw_only=True, weakref_slot=False) +class InteractionChannel(channels.PartialChannel): + """Represents partial channels returned as resolved entities on interactions.""" + + permissions: permissions_.Permissions = attr.field(eq=False, hash=False, repr=True) + """Permissions the command's executor has in this channel.""" + + +@attr_extensions.with_copy +@attr.define(hash=False, kw_only=True, weakref_slot=False) +class ResolvedOptionData: + """Represents the resolved objects of entities referenced in a command's options.""" + + attachments: typing.Mapping[snowflakes.Snowflake, messages.Attachment] = attr.field(repr=False) + """Mapping of snowflake IDs to the attachment objects.""" + + channels: typing.Mapping[snowflakes.Snowflake, InteractionChannel] = attr.field(repr=False) + """Mapping of snowflake IDs to the resolved option partial channel objects.""" + + members: typing.Mapping[snowflakes.Snowflake, InteractionMember] = attr.field(repr=False) + """Mapping of snowflake IDs to the resolved option member objects.""" + + messages: typing.Mapping[snowflakes.Snowflake, messages.Message] = attr.field(repr=False) + """Mapping of snowflake IDs to the resolved option partial message objects.""" + + roles: typing.Mapping[snowflakes.Snowflake, guilds.Role] = attr.field(repr=False) + """Mapping of snowflake IDs to the resolved option role objects.""" + + users: typing.Mapping[snowflakes.Snowflake, users.User] = attr.field(repr=False) + """Mapping of snowflake IDs to the resolved option user objects.""" diff --git a/hikari/interactions/command_interactions.py b/hikari/interactions/command_interactions.py index 014c2b5d74..fce9bab42d 100644 --- a/hikari/interactions/command_interactions.py +++ b/hikari/interactions/command_interactions.py @@ -31,8 +31,6 @@ "CommandInteraction", "COMMAND_RESPONSE_TYPES", "CommandResponseTypesT", - "InteractionChannel", - "ResolvedOptionData", ) import typing @@ -49,7 +47,6 @@ if typing.TYPE_CHECKING: from hikari import guilds - from hikari import messages as messages_ from hikari import permissions as permissions_ from hikari import users as users_ from hikari.api import special_endpoints @@ -77,38 +74,11 @@ * `hikari.interactions.base_interactions.ResponseType.DEFERRED_MESSAGE_CREATE`/`5` """ +InteractionChannel = base_interactions.InteractionChannel +"""Deprecated alias of `hikari.base_interactions.InteractionChannel`.""" -@attr_extensions.with_copy -@attr.define(hash=True, kw_only=True, weakref_slot=False) -class InteractionChannel(channels.PartialChannel): - """Represents partial channels returned as resolved entities on interactions.""" - - permissions: permissions_.Permissions = attr.field(eq=False, hash=False, repr=True) - """Permissions the command's executor has in this channel.""" - - -@attr_extensions.with_copy -@attr.define(hash=False, kw_only=True, weakref_slot=False) -class ResolvedOptionData: - """Represents the resolved objects of entities referenced in a command's options.""" - - attachments: typing.Mapping[snowflakes.Snowflake, messages_.Attachment] = attr.field(repr=False) - """Mapping of snowflake IDs to the attachment objects.""" - - channels: typing.Mapping[snowflakes.Snowflake, InteractionChannel] = attr.field(repr=False) - """Mapping of snowflake IDs to the resolved option partial channel objects.""" - - members: typing.Mapping[snowflakes.Snowflake, base_interactions.InteractionMember] = attr.field(repr=False) - """Mapping of snowflake IDs to the resolved option member objects.""" - - messages: typing.Mapping[snowflakes.Snowflake, messages_.Message] - """Mapping of snowflake IDs to the resolved option partial message objects.""" - - roles: typing.Mapping[snowflakes.Snowflake, guilds.Role] = attr.field(repr=False) - """Mapping of snowflake IDs to the resolved option role objects.""" - - users: typing.Mapping[snowflakes.Snowflake, users_.User] = attr.field(repr=False) - """Mapping of snowflake IDs to the resolved option user objects.""" +ResolvedOptionData = base_interactions.ResolvedOptionData +"""Deprecated alias of `hikari.base_interactions.ResolvedOptionData`.""" @attr_extensions.with_copy @@ -352,7 +322,7 @@ class CommandInteraction( options: typing.Optional[typing.Sequence[CommandInteractionOption]] = attr.field(eq=False, hash=False, repr=True) """Parameter values provided by the user invoking this command.""" - resolved: typing.Optional[ResolvedOptionData] = attr.field(eq=False, hash=False, repr=False) + resolved: typing.Optional[base_interactions.ResolvedOptionData] = attr.field(eq=False, hash=False, repr=False) """Mappings of the objects resolved for the provided command options.""" target_id: typing.Optional[snowflakes.Snowflake] = attr.field(default=None, eq=False, hash=False, repr=True) diff --git a/hikari/interactions/component_interactions.py b/hikari/interactions/component_interactions.py index 7d60820fc8..584e8aa2dc 100644 --- a/hikari/interactions/component_interactions.py +++ b/hikari/interactions/component_interactions.py @@ -107,6 +107,9 @@ class ComponentInteraction( values: typing.Sequence[str] = attr.field(eq=False) """Sequence of the values which were selected for a select menu component.""" + resolved: typing.Optional[base_interactions.ResolvedOptionData] = attr.field(eq=False, hash=False, repr=False) + """Mappings of the objects resolved for the provided command options.""" + guild_id: typing.Optional[snowflakes.Snowflake] = attr.field(eq=False) """ID of the guild this interaction was triggered in. diff --git a/hikari/internal/deprecation.py b/hikari/internal/deprecation.py index ba2f555e43..fc942a02c0 100644 --- a/hikari/internal/deprecation.py +++ b/hikari/internal/deprecation.py @@ -52,10 +52,18 @@ def check_if_past_removal(what: str, /, *, removal_version: str) -> None: If the deprecated item is past its removal version. """ if ux.HikariVersion(hikari_about.__version__) >= ux.HikariVersion(removal_version): - raise DeprecationWarning(f"{what!r} is passed its removal version ({removal_version})") - - -def warn_deprecated(what: str, /, *, removal_version: str, additional_info: str, stack_level: int = 3) -> None: + raise DeprecationWarning(f"{what} is passed its removal version ({removal_version})") + + +def warn_deprecated( + what: str, + /, + *, + removal_version: str, + additional_info: str, + stack_level: int = 3, + quote: bool = True, +) -> None: """Issue a deprecation warning. If the item is past its deprecation version, an error will be raised instead. @@ -73,16 +81,21 @@ def warn_deprecated(what: str, /, *, removal_version: str, additional_info: str, Additional information on the deprecation for the user. stack_level : int The stack level to issue the warning in. + quote : bool + Whether to quote `what` when displaying the deprecation Raises ------ DeprecationWarning If the deprecated item is past its removal version. """ + if quote: + what = repr(what) + check_if_past_removal(what, removal_version=removal_version) warnings.warn( - f"{what!r} is deprecated and will be removed in `{removal_version}`. {additional_info}", + f"{what} is deprecated and will be removed in `{removal_version}`. {additional_info}", category=DeprecationWarning, stacklevel=stack_level, ) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 358129b139..54e937fdc9 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -4362,7 +4362,7 @@ def test__deserialize_resolved_option_data( assert channel.id == 695382395666300958 assert channel.name == "discord-announcements" assert channel.permissions == permission_models.Permissions(17179869183) - assert isinstance(channel, command_interactions.InteractionChannel) + assert isinstance(channel, base_interactions.InteractionChannel) assert len(resolved.members) == 1 member = resolved.members[115590097100865541] assert member == entity_factory_impl._deserialize_interaction_member( @@ -4378,7 +4378,7 @@ def test__deserialize_resolved_option_data( assert resolved.users == {115590097100865541: entity_factory_impl.deserialize_user(user_payload)} assert resolved.messages == {123: entity_factory_impl.deserialize_message(message_payload)} - assert isinstance(resolved, command_interactions.ResolvedOptionData) + assert isinstance(resolved, base_interactions.ResolvedOptionData) def test__deserialize_resolved_option_data_with_empty_resolved_resources(self, entity_factory_impl): resolved = entity_factory_impl._deserialize_resolved_option_data({}) @@ -4807,7 +4807,9 @@ def test_deserialize_context_menu_command_default_member_permissions( assert command.default_member_permissions == permission_models.Permissions.ADMINISTRATOR @pytest.fixture() - def component_interaction_payload(self, interaction_member_payload, message_payload): + def component_interaction_payload( + self, interaction_member_payload, message_payload, interaction_resolved_data_payload + ): return { "version": 1, "type": 3, @@ -4816,7 +4818,12 @@ def component_interaction_payload(self, interaction_member_payload, message_payl "member": interaction_member_payload, "id": "846462639134605312", "guild_id": "290926798626357999", - "data": {"custom_id": "click_one", "component_type": 2, "values": ["1", "2", "67"]}, + "data": { + "custom_id": "click_one", + "component_type": 2, + "values": ["1", "2", "67"], + "resolved": interaction_resolved_data_payload, + }, "channel_id": "345626669114982999", "application_id": "290926444748734465", "locale": "es-ES", @@ -4825,7 +4832,13 @@ def component_interaction_payload(self, interaction_member_payload, message_payl } def test_deserialize_component_interaction( - self, entity_factory_impl, component_interaction_payload, interaction_member_payload, mock_app, message_payload + self, + entity_factory_impl, + component_interaction_payload, + interaction_member_payload, + mock_app, + message_payload, + interaction_resolved_data_payload, ): interaction = entity_factory_impl.deserialize_component_interaction(component_interaction_payload) @@ -4850,6 +4863,10 @@ def test_deserialize_component_interaction( assert interaction.guild_locale == "en-US" assert interaction.guild_locale is locales.Locale.EN_US assert interaction.app_permissions == 5431234 + # ResolvedData + assert interaction.resolved == entity_factory_impl._deserialize_resolved_option_data( + interaction_resolved_data_payload, guild_id=290926798626357999 + ) assert isinstance(interaction, component_interactions.ComponentInteraction) def test_deserialize_component_interaction_with_undefined_fields( @@ -5437,7 +5454,7 @@ def test_deserialize__deserialize_button_with_unset_fields( @pytest.fixture() def select_menu_payload(self, custom_emoji_payload): return { - "type": 3, + "type": 5, "custom_id": "Not an ID", "options": [ { @@ -5454,10 +5471,10 @@ def select_menu_payload(self, custom_emoji_payload): "disabled": True, } - def test__deserialize_select_menu(self, entity_factory_impl, select_menu_payload, custom_emoji_payload): - menu = entity_factory_impl._deserialize_select_menu(select_menu_payload) + def test__deserialize_text_select_menu(self, entity_factory_impl, select_menu_payload, custom_emoji_payload): + menu = entity_factory_impl._deserialize_text_select_menu(select_menu_payload) - assert menu.type is component_models.ComponentType.SELECT_MENU + assert menu.type is component_models.ComponentType.USER_SELECT_MENU assert menu.custom_id == "Not an ID" # SelectMenuOption @@ -5475,8 +5492,8 @@ def test__deserialize_select_menu(self, entity_factory_impl, select_menu_payload assert menu.max_values == 420 assert menu.is_disabled is True - def test__deserialize_select_menu_partial(self, entity_factory_impl): - menu = entity_factory_impl._deserialize_select_menu( + def test__deserialize_text_select_menu_partial(self, entity_factory_impl): + menu = entity_factory_impl._deserialize_text_select_menu( { "type": 3, "custom_id": "Not an ID", @@ -5500,7 +5517,11 @@ def test__deserialize_select_menu_partial(self, entity_factory_impl): ("type_", "fn", "mapping"), [ (2, "_deserialize_button", "_message_component_type_mapping"), - (3, "_deserialize_select_menu", "_message_component_type_mapping"), + (3, "_deserialize_text_select_menu", "_message_component_type_mapping"), + (5, "_deserialize_select_menu", "_message_component_type_mapping"), + (6, "_deserialize_select_menu", "_message_component_type_mapping"), + (7, "_deserialize_select_menu", "_message_component_type_mapping"), + (8, "_deserialize_channel_select_menu", "_message_component_type_mapping"), (4, "_deserialize_text_input", "_modal_component_type_mapping"), ], ) diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py index 6454c121a4..a5edf56ba8 100644 --- a/tests/hikari/impl/test_special_endpoints.py +++ b/tests/hikari/impl/test_special_endpoints.py @@ -23,6 +23,7 @@ import mock import pytest +from hikari import channels from hikari import commands from hikari import components from hikari import emojis @@ -1438,21 +1439,11 @@ def test_build_partial(self, option): class TestSelectMenuBuilder: @pytest.fixture() def menu(self): - return special_endpoints.SelectMenuBuilder(container=mock.Mock(), custom_id="o2o2o2") + return special_endpoints.SelectMenuBuilder(container=mock.Mock(), custom_id="o2o2o2", type=5) def test_custom_id_property(self, menu): assert menu.custom_id == "o2o2o2" - def test_add_add_option(self, menu): - option = menu.add_option("ok", "no u") - option.add_to_menu() - assert menu.options == [option] - - def test_add_raw_option(self, menu): - mock_option = object() - menu.add_raw_option(mock_option) - assert menu.options == [mock_option] - def test_set_is_disabled(self, menu): assert menu.set_is_disabled(True) is menu assert menu.is_disabled is True @@ -1474,36 +1465,73 @@ def test_add_to_container(self, menu): menu._container.add_component.assert_called_once_with(menu) def test_build(self): - result = special_endpoints.SelectMenuBuilder(container=object(), custom_id="o2o2o2").build() + result = special_endpoints.SelectMenuBuilder(container=object(), custom_id="o2o2o2", type=5).build() assert result == { - "type": components.ComponentType.SELECT_MENU, + "type": 5, "custom_id": "o2o2o2", - "options": [], "disabled": False, "min_values": 0, "max_values": 1, } - def test_build_partial(self): + +class TestTextSelectMenuBuilder: + @pytest.fixture() + def menu(self): + return special_endpoints.TextSelectMenuBuilder(container=mock.Mock(), custom_id="o2o2o2") + + def test_add_add_option(self, menu): + option = menu.add_option("ok", "no u") + option.add_to_menu() + assert menu.options == [option] + + def test_add_raw_option(self, menu): + mock_option = object() + menu.add_raw_option(mock_option) + assert menu.options == [mock_option] + + def test_build(self): + result = ( + special_endpoints.TextSelectMenuBuilder(container=object(), custom_id="o2o2o2") + .set_placeholder("hi") + .set_min_values(22) + .set_max_values(53) + .set_is_disabled(True) + .build() + ) + + assert result == { + "type": 3, + "custom_id": "o2o2o2", + "placeholder": "hi", + "min_values": 22, + "max_values": 53, + "disabled": True, + "options": [], + } + + +class TestChannelSelectMenuBuilder: + def test_build(self): result = ( - special_endpoints.SelectMenuBuilder(container=object(), custom_id="o2o2o2") + special_endpoints.ChannelSelectMenuBuilder(container=object(), custom_id="o2o2o2") .set_placeholder("hi") .set_min_values(22) .set_max_values(53) .set_is_disabled(True) - .add_raw_option(mock.Mock(build=mock.Mock(return_value={"hi": "OK"}))) + .set_channel_types([channels.ChannelType.GUILD_CATEGORY]) .build() ) assert result == { - "type": components.ComponentType.SELECT_MENU, + "type": 8, "custom_id": "o2o2o2", - "options": [{"hi": "OK"}], "placeholder": "hi", "min_values": 22, "max_values": 53, "disabled": True, + "channel_types": [channels.ChannelType.GUILD_CATEGORY], } diff --git a/tests/hikari/interactions/test_component_interactions.py b/tests/hikari/interactions/test_component_interactions.py index b4427b92c8..3ad1f07de4 100644 --- a/tests/hikari/interactions/test_component_interactions.py +++ b/tests/hikari/interactions/test_component_interactions.py @@ -55,6 +55,7 @@ def mock_component_interaction(self, mock_app): locale="es-ES", guild_locale="en-US", app_permissions=123321, + resolved=None, ) def test_build_response(self, mock_component_interaction, mock_app):