From 5b7d5244d22dcada1fa62ce00ef5fb28a2e1455b Mon Sep 17 00:00:00 2001 From: sadru Date: Fri, 4 Feb 2022 00:30:01 +0100 Subject: [PATCH 01/40] Partial modal implementation --- TODO | 13 ++ hikari/api/rest.py | 28 ++++ hikari/api/special_endpoints.py | 8 ++ hikari/impl/entity_factory.py | 52 ++++++++ hikari/impl/rest.py | 23 ++++ hikari/impl/special_endpoints.py | 71 ++++++++++ hikari/interactions/base_interactions.py | 43 ++++++ hikari/interactions/modal_interactions.py | 156 ++++++++++++++++++++++ hikari/messages.py | 11 ++ 9 files changed, 405 insertions(+) create mode 100644 TODO create mode 100644 hikari/interactions/modal_interactions.py diff --git a/TODO b/TODO new file mode 100644 index 0000000000..6d26d1b5d4 --- /dev/null +++ b/TODO @@ -0,0 +1,13 @@ +[ ] modal interaction response (sent to user) + [ ] builder + [x] rest method +[ ] modal interaction event (gotten from user) + [x] all attributes + [x] respond with rest + [ ] respond with restbot +[ ] text input + [x] interaction model + [ ] response builder +[x] factory + [x] modal + [x] text input diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 0f76717cc5..b0765b9548 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -7822,6 +7822,34 @@ async def create_autocomplete_response( If an internal error occurs on Discord while handling the request. """ # noqa: E501 - Line too long + async def create_modal_response( + self, + interaction: snowflakes.SnowflakeishOr[base_interactions.PartialInteraction], + token: str, + *, + title: str, + custom_id: str, + components: typing.Sequence[special_endpoints.ComponentBuilder], + ) -> None: + """Create a response by sending a modal. + + Parameters + ---------- + interaction : hikari.snowflakes.SnowflakeishOr[hikari.interactions.base_interactions.PartialInteraction] + Object or ID of the interaction this response is for. + token : builtins.str + The command interaction's token. + + Other Parameters + ---------------- + title : str + The title that will show up in the modal. + custom_id : str + Developer set custom ID used for identifying interactions with this modal. + components : typing.Sequence[special_endpoints.ComponentBuilder] + The components to display in the modal. + """ + @abc.abstractmethod def build_action_row(self) -> special_endpoints.ActionRowBuilder: """Build an action row message component for use in message create and REST calls. diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 3777e52c44..994abff80c 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -40,6 +40,7 @@ "LinkButtonBuilder", "SelectMenuBuilder", "SelectOptionBuilder", + "TextInputBuilder", ] import abc @@ -1549,6 +1550,13 @@ def add_to_container(self) -> _ContainerT: """ +class TextInputBuilder(ComponentBuilder, abc.ABC): + """Builder class for text inputs components.""" + + __slots__: typing.Sequence[str] = () + + # TODO(modal): add abstract + class ActionRowBuilder(ComponentBuilder, abc.ABC): """Builder class for action row components.""" diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 72aecf1920..91058cbe36 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -59,6 +59,7 @@ from hikari.interactions import base_interactions from hikari.interactions import command_interactions from hikari.interactions import component_interactions +from hikari.interactions import modal_interactions from hikari.internal import attr_extensions from hikari.internal import data_binding from hikari.internal import time @@ -261,6 +262,7 @@ def __init__(self, app: traits.RESTAware) -> None: message_models.ComponentType.ACTION_ROW: self.deserialize_action_row, message_models.ComponentType.BUTTON: self.deserialize_button, message_models.ComponentType.SELECT_MENU: self.deserialize_select_menu, + message_models.ComponentType.TEXT_INPUT: self.deserialize_text_input, } self._dm_channel_type_mapping = { channel_models.ChannelType.DM: self.deserialize_dm, @@ -280,6 +282,7 @@ def __init__(self, app: traits.RESTAware) -> None: base_interactions.InteractionType.APPLICATION_COMMAND: self.deserialize_command_interaction, base_interactions.InteractionType.MESSAGE_COMPONENT: self.deserialize_component_interaction, base_interactions.InteractionType.AUTOCOMPLETE: self.deserialize_autocomplete_interaction, + base_interactions.InteractionType.MODAL_SUBMIT: self.deserialize_modal_interaction, } self._webhook_type_mapping = { webhook_models.WebhookType.INCOMING: self.deserialize_incoming_webhook, @@ -2044,6 +2047,48 @@ def deserialize_autocomplete_interaction( locale=payload["locale"], guild_locale=payload.get("guild_locale"), ) + + def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> modal_interactions.ModalInteraction: + data_payload = payload["data"] + + guild_id: typing.Optional[snowflakes.Snowflake] = None + if raw_guild_id := payload.get("guild_id"): + guild_id = snowflakes.Snowflake(raw_guild_id) + + member: typing.Optional[base_interactions.InteractionMember] + if member_payload := payload.get("member"): + assert guild_id is not None + member = self._deserialize_interaction_member(member_payload, guild_id=guild_id) + # See https://github.com/discord/discord-api-docs/pull/2568 + user = member.user + + else: + member = None + user = self.deserialize_user(payload["user"]) + + components: typing.List[message_models.PartialComponent] = [] + for component_payload in data_payload["components"]: + try: + components.append(self.deserialize_component(component_payload)) + except errors.UnrecognisedEntityError: + pass + + return modal_interactions.ModalInteraction( + app=self._app, + application_id=snowflakes.Snowflake(payload["application_id"]), + id=snowflakes.Snowflake(payload["id"]), + type=base_interactions.InteractionType(payload["type"]), + guild_id=guild_id, + guild_locale=payload.get("guild_locale", "en-US"), + locale=payload["locale"], + channel_id=snowflakes.Snowflake(payload["channel_id"]), + member=member, + user=user, + token=payload["token"], + version=payload["version"], + custom_id=data_payload["custom_id"], + components=components, + ) def deserialize_interaction(self, payload: data_binding.JSONObject) -> base_interactions.PartialInteraction: interaction_type = base_interactions.InteractionType(payload["type"]) @@ -2225,6 +2270,13 @@ def deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_m max_values=payload.get("max_values", 1), is_disabled=payload.get("disabled", False), ) + + def deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.ModalInteractionTextInput: + return modal_interactions.ModalInteractionTextInput( + type=message_models.ComponentType(payload["type"]), + custom_id=payload["custom_id"], + value=payload["value"] + ) def deserialize_component(self, payload: data_binding.JSONObject) -> message_models.PartialComponent: component_type = message_models.ComponentType(payload["type"]) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 260164a954..83d9a1e33f 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -3528,5 +3528,28 @@ async def create_autocomplete_response( body.put("data", data) await self._request(route, json=body, no_auth=True) + async def create_modal_response( + self, + interaction: snowflakes.SnowflakeishOr[base_interactions.PartialInteraction], + token: str, + *, + title: str, + custom_id: str, + components: typing.Sequence[special_endpoints.ComponentBuilder], + ) -> None: + route = routes.POST_INTERACTION_RESPONSE.compile(interaction=interaction, token=token) + + body = data_binding.JSONObjectBuilder() + body.put("type", base_interactions.ResponseType.MODAL) + + data = data_binding.JSONObjectBuilder() + data.put("title", title) + data.put("custom_id", custom_id) + data.put("components", [component.build() for component in components]) + + body.put("data", data) + + await self._request(route, json=body, no_auth=True) + def build_action_row(self) -> special_endpoints.ActionRowBuilder: return special_endpoints_impl.ActionRowBuilder() diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 9d6fdc4f98..81766040af 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -39,6 +39,7 @@ "InteractiveButtonBuilder", "LinkButtonBuilder", "SelectMenuBuilder", + "TextInputBuilder", ] import asyncio @@ -1295,6 +1296,76 @@ def build(self) -> data_binding.JSONObject: data.put("disabled", self._is_disabled) return data +class TextInputBuilder(special_endpoints.TextInputBuilder): + """Standard implementation of `hikari.api.special_endpoints.TextInputBuilder`.""" + + # TODO(modal): actually define TextStyleType somewhere (where?) + _style: TextStyleType = attr.field() + _custom_id: str = attr.field() + _label: str = attr.field() + + _placeholder: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED) + _value: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED) + _required: undefined.UndefinedOr[bool] = attr.field(default=undefined.UNDEFINED) + _min_length: undefined.UndefinedOr[int] = attr.field(default=undefined.UNDEFINED) + _max_length: undefined.UndefinedOr[int] = attr.field(default=undefined.UNDEFINED) + + @property + def style(self) -> TextStyleType: + return self._style + + @property + def custom_id(self) -> str: + return self._custom_id + + + @property + def label(self) -> str: + return self._label + + @property + def placeholder(self) -> undefined.UndefinedOr[str]: + return self._placeholder + + + @property + def value(self) -> undefined.UndefinedOr[str]: + return self._value + + + @property + def required(self) -> undefined.UndefinedOr[bool]: + return self._required + + + @property + def min_length(self) -> undefined.UndefinedOr[int]: + return self._min_length + + + @property + def max_length(self) -> undefined.UndefinedOr[int]: + return self._max_length + + # TODO(modal): setters + + def build(self) -> data_binding.JSONObject: + data = data_binding.JSONObjectBuilder() + + data["type"] = messages.ComponentType.TEXT_INPUT + data["style"] = self._style + data["custom_id"] = self._custom_id + data["label"] = self._label + data.put("placeholder", self._placeholder) + data.put("value", self._value) + data.put("required", self._required) + data.put("min_length", self._min_length) + data.put("max_length", self._max_length) + + return data + + + @attr.define(kw_only=True, weakref_slot=False) class ActionRowBuilder(special_endpoints.ActionRowBuilder): diff --git a/hikari/interactions/base_interactions.py b/hikari/interactions/base_interactions.py index 019547adb6..614bc9d60d 100644 --- a/hikari/interactions/base_interactions.py +++ b/hikari/interactions/base_interactions.py @@ -74,6 +74,9 @@ class InteractionType(int, enums.Enum): AUTOCOMPLETE = 4 """An interaction triggered by a user typing in a slash command option.""" + MODAL_SUBMIT = 5 + """An interaction triggered by a user submitting a modal.""" + @typing.final class ResponseType(int, enums.Enum): @@ -126,6 +129,14 @@ class ResponseType(int, enums.Enum): * `InteractionType.AUTOCOMPLETE` """ + MODAL = 9 + """An immediate interaction response with instructions to display a modal. + + This is valid for the following interaction types: + + * `InteractionType.MODAL_SUBMIT` + """ + MESSAGE_RESPONSE_TYPES: typing.Final[typing.AbstractSet[MessageResponseTypesT]] = frozenset( [ResponseType.MESSAGE_CREATE, ResponseType.MESSAGE_UPDATE] @@ -374,6 +385,38 @@ async def create_initial_response( role_mentions=role_mentions, ) + async def create_modal_response( + self, + title: str, + custom_id: str, + components: typing.Sequence[special_endpoints.ComponentBuilder], + ) -> None: + """Create a response by sending a modal. + + Parameters + ---------- + interaction : hikari.snowflakes.SnowflakeishOr[hikari.interactions.base_interactions.PartialInteraction] + Object or ID of the interaction this response is for. + token : builtins.str + The command interaction's token. + + Other Parameters + ---------------- + title : str + The title that will show up in the modal. + custom_id : str + Developer set custom ID used for identifying interactions with this modal. + components : typing.Sequence[special_endpoints.ComponentBuilder] + The components to display in the modal. + """ + await self.app.rest.create_modal_response( + self.id, + self.token, + title=title, + custom_id=custom_id, + components=components, + ) + async def edit_initial_response( self, content: undefined.UndefinedNoneOr[typing.Any] = undefined.UNDEFINED, diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py new file mode 100644 index 0000000000..9ab60e581d --- /dev/null +++ b/hikari/interactions/modal_interactions.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021 davfsa +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Models and enums used for Discord's Modals interaction flow.""" + +from __future__ import annotations + +__all__: typing.List[str] = ["ModalResponseTypesT", "ModalInteraction", "ModalInteractionTextInput", "ModalInteraction"] + +import typing + +import attr + +from hikari import messages +from hikari import snowflakes +from hikari.interactions import base_interactions +from hikari.internal import attr_extensions + +if typing.TYPE_CHECKING: + from hikari import users as _users + from hikari.api import special_endpoints + +ModalResponseTypesT = typing.Literal[ + base_interactions.ResponseType.MESSAGE_CREATE, 4, base_interactions.ResponseType.DEFERRED_MESSAGE_CREATE, 5 +] +"""Type-hint of the response types which are valid for a modal interaction. + +The following types are valid for this: + +* `hikari.interactions.base_interactions.ResponseType.MESSAGE_CREATE`/`4` +* `hikari.interactions.base_interactions.ResponseType.DEFERRED_MESSAGE_CREATE`/`5` +""" + + +@attr.define(kw_only=True, weakref_slot=False) +class ModalInteractionTextInput(messages.PartialComponent): + """A text input component in a modal interaction""" + + custom_id: str = attr.field(repr=True) + """Developer set custom ID used for identifying interactions with this modal.""" + value: str = attr.field(repr=True) + """Value provided for this text input.""" + + + +@attr_extensions.with_copy +@attr.define(hash=True, kw_only=True, weakref_slot=False) +class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypesT]): + """Represents a modal interaction on Discord.""" + + channel_id: snowflakes.Snowflake = attr.field(eq=False, hash=False, repr=True) + """ID of the channel this modal interaction event was triggered in.""" + + guild_id: typing.Optional[snowflakes.Snowflake] = attr.field(eq=False, hash=False, repr=True) + """ID of the guild this modal interaction event was triggered in. + + This will be `builtins.None` for modal interactions triggered in DMs. + """ + + guild_locale: typing.Optional[str] = attr.field(eq=False, hash=False, repr=True) + """The preferred language of the guild this modal interaction was triggered in. + + This will be `builtins.None` for modal interactions triggered in DMs. + + !!! note + This value can usually only be changed if `COMMUNITY` is in `hikari.guilds.Guild.features` + for the guild and will otherwise default to `en-US`. + """ + + member: typing.Optional[base_interactions.InteractionMember] = attr.field(eq=False, hash=False, repr=True) + """The member who triggered this modal interaction. + + This will be `builtins.None` for modal interactions triggered in DMs. + + !!! note + This member object comes with the extra field `permissions` which + contains the member's permissions in the current channel. + """ + + user: _users.User = attr.field(eq=False, hash=False, repr=True) + """The user who triggered this modal interaction.""" + + locale: str = attr.field(eq=False, hash=False, repr=True) + """The selected language of the user who triggered this Modal interaction.""" + + custom_id: str = attr.field(eq=False, hash=False, repr=True) + """The custom id of the modal.""" + + components: typing.Sequence[messages.PartialComponent] = attr.field(eq=False, hash=False, repr=True) + """Components in the modal.""" + + def build_response(self) -> special_endpoints.InteractionMessageBuilder: + """Get a message response builder for use in the REST server flow. + + !!! note + For interactions received over the gateway + `ModalInteraction.create_initial_response` should be used to set + the interaction response message. + + Examples + -------- + ```py + async def handle_modal_interaction(interaction: ModalInteraction) -> InteractionMessageBuilder: + return ( + interaction + .build_response() + .add_embed(Embed(description="Hi there")) + .set_content("Konnichiwa") + ) + ``` + + Returns + ------- + hikari.api.special_endpoints.InteractionMessageBuilder + Interaction message response builder object. + """ + return self.app.rest.interaction_message_builder(base_interactions.ResponseType.MESSAGE_CREATE) + + def build_deferred_response(self) -> special_endpoints.InteractionDeferredBuilder: + """Get a deferred message response builder for use in the REST server flow. + + !!! note + For interactions received over the gateway + `ModalInteraction.create_initial_response` should be used to set + the interaction response message. + + !!! note + Unlike `hikari.api.special_endpoints.InteractionMessageBuilder`, + the result of this call can be returned as is without any modifications + being made to it. + + Returns + ------- + hikari.api.special_endpoints.InteractionDeferredBuilder + Deferred interaction message response builder object. + """ + return self.app.rest.interaction_deferred_builder(base_interactions.ResponseType.DEFERRED_MESSAGE_CREATE) diff --git a/hikari/messages.py b/hikari/messages.py index 9984ab9f66..a528c7fddf 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -525,6 +525,17 @@ class ComponentType(int, enums.Enum): as `ComponentType.ACTION_ROW`. """ + TEXT_INPUT = 4 + """A text input component + + !! note + This component may only be used in modals. + + !!! 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): From 0f5933ce71869ac4f1f8264b54c2289d9a35af26 Mon Sep 17 00:00:00 2001 From: sadru Date: Fri, 4 Feb 2022 12:43:49 +0100 Subject: [PATCH 02/40] Add abstract methods and documentation --- TODO | 8 + hikari/api/special_endpoints.py | 259 +++++++++++++++++++++- hikari/impl/entity_factory.py | 14 +- hikari/impl/rest.py | 8 +- hikari/impl/special_endpoints.py | 90 +++++--- hikari/interactions/modal_interactions.py | 22 +- hikari/messages.py | 10 + 7 files changed, 364 insertions(+), 47 deletions(-) diff --git a/TODO b/TODO index 6d26d1b5d4..f47833fdda 100644 --- a/TODO +++ b/TODO @@ -8,6 +8,14 @@ [ ] text input [x] interaction model [ ] response builder + [x] builder classes + [ ] helper methods [x] factory [x] modal [x] text input +[ ] tests + [ ] modal interaction response + [ ] rest + [ ] helper methods + [ ] modal interaction event + [ ] entity factory diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 994abff80c..b01b0bd922 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -1550,12 +1550,233 @@ def add_to_container(self) -> _ContainerT: """ -class TextInputBuilder(ComponentBuilder, abc.ABC): +class TextInputBuilder(ComponentBuilder, abc.ABC, typing.Generic[_ContainerT]): """Builder class for text inputs components.""" - + __slots__: typing.Sequence[str] = () - - # TODO(modal): add abstract + + @property + @abc.abstractmethod + def style(self) -> messages.TextInputStyle: + """Style to use for the text input. + + Returns + ------- + builtins.str + Style to use for the text input. + """ + + @property + @abc.abstractmethod + def custom_id(self) -> str: + """Developer set custom ID used for identifying this text input. + + !!! note + This custom_id is never used in component interaction events. + It is meant to be used purely for resolving components modal interactions. + + Returns + ------- + builtins.str + Developer set custom ID used for identifying this text input. + """ + + @property + @abc.abstractmethod + def label(self) -> str: + """Label above this text input. + + Returns + ------- + builtins.str + Label above this text input. + """ + + @property + @abc.abstractmethod + def placeholder(self) -> undefined.UndefinedOr[str]: + """Return the placeholder text that will disappear when the user types anything. + + Returns + ------- + hikari.undefined.UndefinedOr[builtins.str] + Placeholder text that will disappear when the user types anything. + """ + + @property + @abc.abstractmethod + def value(self) -> undefined.UndefinedOr[str]: + """Pre-filled text that will be sent if the user does not write anything. + + Returns + ------- + hikari.undefined.UndefinedOr[builtins.str] + Pre-filled text that will be sent if the user does not write anything. + """ + + @property + @abc.abstractmethod + def required(self) -> undefined.UndefinedOr[bool]: + """Whether this text input is required to be filled-in. + + Returns + ------- + hikari.undefined.UndefinedOr[builtins.bool] + Whether this text input is required to be filled-in. + """ + + @property + @abc.abstractmethod + def min_length(self) -> undefined.UndefinedOr[int]: + """Return the minimum length the text should have. + + Returns + ------- + hikari.undefined.UndefinedOr[builtins.int] + The minimum length the text should have. + """ + + @property + @abc.abstractmethod + def max_length(self) -> undefined.UndefinedOr[int]: + """Return the maxmimum length the text should have. + + Returns + ------- + hikari.undefined.UndefinedOr[builtins.int] + The maxmimum length the text should have. + """ + + @abc.abstractmethod + def set_style(self: _T, style: typing.Union[messages.TextInputStyle, int], /) -> _T: + """Set the style to use for the text input. + + Parameters + ---------- + style : typing.Union[hikari.messages.TextInputStyle, int] + Style to use for the text input. + + Returns + ------- + TextInputBuilder + The builder object to enable chained calls. + """ + + @abc.abstractmethod + def set_custom_id(self: _T, custom_id: str, /) -> _T: + """Set the developer set custom ID used for identifying this text input. + + Parameters + ---------- + custom_id : builtins.str + Developer set custom ID used for identifying this text input. + + Returns + ------- + TextInputBuilder + The builder object to enable chained calls. + """ + + @abc.abstractmethod + def set_label(self: _T, label: str, /) -> _T: + """Set the label above this text input. + + Parameters + ---------- + label : builtins.str + Label above this text input. + + Returns + ------- + TextInputBuilder + The builder object to enable chained calls. + """ + + @abc.abstractmethod + def set_placeholder(self: _T, placeholder: str, /) -> _T: + """Return the placeholder text that will disappear when the user types anything. + + Parameters + ---------- + placeholder : builtins.str: + Placeholder text that will disappear when the user types anything. + + Returns + ------- + TextInputBuilder + The builder object to enable chained calls. + """ + + @abc.abstractmethod + def set_value(self: _T, value: str, /) -> _T: + """Pre-filled text that will be sent if the user does not write anything. + + Parameters + ---------- + value : builtins.str + Pre-filled text that will be sent if the user does not write anything. + + Returns + ------- + TextInputBuilder + The builder object to enable chained calls. + """ + + @abc.abstractmethod + def set_required(self: _T, required: bool, /) -> _T: + """Set whether this text input is required to be filled-in. + + Parameters + ---------- + required : builtins.bool + Whether this text input is required to be filled-in. + + Returns + ------- + TextInputBuilder + The builder object to enable chained calls. + """ + + @abc.abstractmethod + def set_min_length(self: _T, min_length: int, /) -> _T: + """Set the minimum length the text should have. + + Parameters + ---------- + min_length : builtins.int + The minimum length the text should have. + + Returns + ------- + TextInputBuilder + The builder object to enable chained calls. + """ + + @abc.abstractmethod + def set_max_length(self: _T, max_length: int, /) -> _T: + """Set the maxmimum length the text should have. + + Parameters + ---------- + max_length : builtins.int + The maxmimum length the text should have. + + Returns + ------- + TextInputBuilder + The builder object to enable chained calls. + """ + + @abc.abstractmethod + def add_to_container(self) -> _ContainerT: + """Finalise this builder by adding it to its parent container component. + + Returns + ------- + _ContainerT + The parent container component builder. + """ + class ActionRowBuilder(ComponentBuilder, abc.ABC): """Builder class for action row components.""" @@ -1638,7 +1859,7 @@ def add_button( typing.Union[LinkButtonBuilder[Self], InteractiveButtonBuilder[Self]] Button builder object. `ButtonBuilder.add_to_container` should be called to finalise the - button. + component. """ @abc.abstractmethod @@ -1656,5 +1877,31 @@ def add_select_menu(self: _T, custom_id: str, /) -> SelectMenuBuilder[_T]: SelectMenuBuilder[Self] Select menu builder object. `SelectMenuBuilder.add_to_container` should be called to finalise the - button. + component. + """ + + @abc.abstractmethod + def add_text_input( + self: _T, + style: typing.Union[messages.TextInputStyle, int], + custom_id: str, + label: str, + ) -> TextInputBuilder[_T]: + """Add a text input component to this action row builder. + + Parameters + ---------- + style : typing.Union[hikari.messages.TextInputStyle, int] + Style to use for the text input. + custom_id : builtins.str + Developer set custom ID used for identifying this text input. + label : builtins.str + Label above this text input. + + Returns + ------- + TextInputBuilder[Self] + Text input builder object. + `TextInputBuilder.add_to_container` should be called to finalise the + component. """ diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 91058cbe36..df72cb6782 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -2047,14 +2047,14 @@ def deserialize_autocomplete_interaction( locale=payload["locale"], guild_locale=payload.get("guild_locale"), ) - + def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> modal_interactions.ModalInteraction: data_payload = payload["data"] guild_id: typing.Optional[snowflakes.Snowflake] = None if raw_guild_id := payload.get("guild_id"): guild_id = snowflakes.Snowflake(raw_guild_id) - + member: typing.Optional[base_interactions.InteractionMember] if member_payload := payload.get("member"): assert guild_id is not None @@ -2065,8 +2065,8 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod else: member = None user = self.deserialize_user(payload["user"]) - - components: typing.List[message_models.PartialComponent] = [] + + components: typing.List[typing.Any] = [] for component_payload in data_payload["components"]: try: components.append(self.deserialize_component(component_payload)) @@ -2270,12 +2270,10 @@ def deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_m max_values=payload.get("max_values", 1), is_disabled=payload.get("disabled", False), ) - + def deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.ModalInteractionTextInput: return modal_interactions.ModalInteractionTextInput( - type=message_models.ComponentType(payload["type"]), - custom_id=payload["custom_id"], - value=payload["value"] + type=message_models.ComponentType(payload["type"]), custom_id=payload["custom_id"], value=payload["value"] ) def deserialize_component(self, payload: data_binding.JSONObject) -> message_models.PartialComponent: diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 83d9a1e33f..4784a28ab6 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -3538,17 +3538,17 @@ async def create_modal_response( components: typing.Sequence[special_endpoints.ComponentBuilder], ) -> None: route = routes.POST_INTERACTION_RESPONSE.compile(interaction=interaction, token=token) - + body = data_binding.JSONObjectBuilder() body.put("type", base_interactions.ResponseType.MODAL) - + data = data_binding.JSONObjectBuilder() data.put("title", title) data.put("custom_id", custom_id) data.put("components", [component.build() for component in components]) - + body.put("data", data) - + await self._request(route, json=body, no_auth=True) def build_action_row(self) -> special_endpoints.ActionRowBuilder: diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 81766040af..ed9282e35d 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -90,6 +90,7 @@ _ButtonBuilderT = typing.TypeVar("_ButtonBuilderT", bound="_ButtonBuilder[typing.Any]") _SelectOptionBuilderT = typing.TypeVar("_SelectOptionBuilderT", bound="_SelectOptionBuilder[typing.Any]") _SelectMenuBuilderT = typing.TypeVar("_SelectMenuBuilderT", bound="SelectMenuBuilder[typing.Any]") + _TextInputBuilderT = typing.TypeVar("_TextInputBuilderT", bound="TextInputBuilder[typing.Any]") # Hack around used to avoid recursive generic types leading to type checker issues in builders class _ContainerProto(typing.Protocol): @@ -1296,59 +1297,91 @@ def build(self) -> data_binding.JSONObject: data.put("disabled", self._is_disabled) return data -class TextInputBuilder(special_endpoints.TextInputBuilder): + +@attr_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +class TextInputBuilder(special_endpoints.TextInputBuilder[_ContainerProtoT]): """Standard implementation of `hikari.api.special_endpoints.TextInputBuilder`.""" - - # TODO(modal): actually define TextStyleType somewhere (where?) - _style: TextStyleType = attr.field() + + _container: _ContainerProtoT = attr.field() + _style: messages.TextInputStyle = attr.field() _custom_id: str = attr.field() _label: str = attr.field() - _placeholder: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED) - _value: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED) - _required: undefined.UndefinedOr[bool] = attr.field(default=undefined.UNDEFINED) - _min_length: undefined.UndefinedOr[int] = attr.field(default=undefined.UNDEFINED) - _max_length: undefined.UndefinedOr[int] = attr.field(default=undefined.UNDEFINED) - + _placeholder: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED, kw_only=True) + _value: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED, kw_only=True) + _required: undefined.UndefinedOr[bool] = attr.field(default=undefined.UNDEFINED, kw_only=True) + _min_length: undefined.UndefinedOr[int] = attr.field(default=undefined.UNDEFINED, kw_only=True) + _max_length: undefined.UndefinedOr[int] = attr.field(default=undefined.UNDEFINED, kw_only=True) + @property - def style(self) -> TextStyleType: + def style(self) -> messages.TextInputStyle: return self._style - + @property def custom_id(self) -> str: return self._custom_id - - + @property def label(self) -> str: return self._label - + @property def placeholder(self) -> undefined.UndefinedOr[str]: return self._placeholder - @property def value(self) -> undefined.UndefinedOr[str]: return self._value - @property def required(self) -> undefined.UndefinedOr[bool]: return self._required - @property def min_length(self) -> undefined.UndefinedOr[int]: return self._min_length - @property def max_length(self) -> undefined.UndefinedOr[int]: return self._max_length - - # TODO(modal): setters - + + def set_style(self: _TextInputBuilderT, style: typing.Union[messages.TextInputStyle, int], /) -> _TextInputBuilderT: + self._style = messages.TextInputStyle(style) + return self + + def set_custom_id(self: _TextInputBuilderT, custom_id: str, /) -> _TextInputBuilderT: + self._custom_id = custom_id + return self + + def set_label(self: _TextInputBuilderT, label: str, /) -> _TextInputBuilderT: + self._label = label + return self + + def set_placeholder(self: _TextInputBuilderT, placeholder: str, /) -> _TextInputBuilderT: + self._placeholder = placeholder + return self + + def set_value(self: _TextInputBuilderT, value: str, /) -> _TextInputBuilderT: + self._value = value + return self + + def set_required(self: _TextInputBuilderT, required: bool, /) -> _TextInputBuilderT: + self._required = required + return self + + def set_min_length(self: _TextInputBuilderT, min_length: int, /) -> _TextInputBuilderT: + self._min_length = min_length + return self + + def set_max_length(self: _TextInputBuilderT, max_length: int, /) -> _TextInputBuilderT: + self._max_length = max_length + return self + + def add_to_container(self) -> _ContainerProtoT: + self._container.add_component(self) + return self._container + def build(self) -> data_binding.JSONObject: data = data_binding.JSONObjectBuilder() @@ -1361,10 +1394,8 @@ def build(self) -> data_binding.JSONObject: data.put("required", self._required) data.put("min_length", self._min_length) data.put("max_length", self._max_length) - - return data - + return data @attr.define(kw_only=True, weakref_slot=False) @@ -1432,6 +1463,15 @@ def add_select_menu( self._assert_can_add_type(messages.ComponentType.SELECT_MENU) return SelectMenuBuilder(container=self, custom_id=custom_id) + def add_text_input( + self: _ActionRowBuilderT, + style: typing.Union[messages.TextInputStyle, int], + custom_id: str, + label: str, + ) -> special_endpoints.TextInputBuilder[_ActionRowBuilderT]: + self._assert_can_add_type(messages.ComponentType.TEXT_INPUT) + return TextInputBuilder(container=self, style=messages.TextInputStyle(style), custom_id=custom_id, label=label) + def build(self) -> data_binding.JSONObject: return { "type": messages.ComponentType.ACTION_ROW, diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index 9ab60e581d..ef6fa20f2a 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -24,7 +24,12 @@ from __future__ import annotations -__all__: typing.List[str] = ["ModalResponseTypesT", "ModalInteraction", "ModalInteractionTextInput", "ModalInteraction"] +__all__: typing.List[str] = [ + "ModalResponseTypesT", + "ModalInteraction", + "ModalInteractionTextInput", + "ModalInteraction", +] import typing @@ -53,13 +58,22 @@ @attr.define(kw_only=True, weakref_slot=False) class ModalInteractionTextInput(messages.PartialComponent): - """A text input component in a modal interaction""" + """A text input component in a modal interaction.""" custom_id: str = attr.field(repr=True) """Developer set custom ID used for identifying interactions with this modal.""" + value: str = attr.field(repr=True) """Value provided for this text input.""" - + + +class ModalInteractionActionRow(typing.Protocol): + """An action row with only partial text inputs. + + Meant purely for use with ModalInteraction. + """ + + components: typing.List[ModalInteractionTextInput] @attr_extensions.with_copy @@ -105,7 +119,7 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes custom_id: str = attr.field(eq=False, hash=False, repr=True) """The custom id of the modal.""" - components: typing.Sequence[messages.PartialComponent] = attr.field(eq=False, hash=False, repr=True) + components: typing.Sequence[ModalInteractionActionRow] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" def build_response(self) -> special_endpoints.InteractionMessageBuilder: diff --git a/hikari/messages.py b/hikari/messages.py index a528c7fddf..398d6ed104 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -39,6 +39,7 @@ "ActionRowComponent", "ButtonComponent", "ButtonStyle", + "TextInputStyle", "SelectMenuOption", "SelectMenuComponent", "InteractiveButtonTypes", @@ -600,6 +601,15 @@ class ButtonStyle(int, enums.Enum): """ +class TextInputStyle(int, enums.Enum): + """A text input style.""" + + SHORT = 1 + """Intended for short single-line text.""" + PARAGRAPH = 2 + """Intended for much longer inputs.""" + + @attr.define(kw_only=True, weakref_slot=False) class PartialComponent: """Base class for all component entities.""" From 3514b900c6870a05294fd2064ce8edb4b1f728ff Mon Sep 17 00:00:00 2001 From: sadru Date: Fri, 4 Feb 2022 14:40:28 +0100 Subject: [PATCH 03/40] Add modal response builders --- TODO | 21 ---- changes/1002.feature.md | 1 + hikari/api/rest.py | 27 ++++- hikari/api/special_endpoints.py | 102 ++++++++++++++++++ hikari/impl/rest.py | 15 ++- hikari/impl/special_endpoints.py | 50 +++++++++ hikari/interactions/command_interactions.py | 25 +++++ hikari/interactions/component_interactions.py | 26 +++++ 8 files changed, 244 insertions(+), 23 deletions(-) delete mode 100644 TODO create mode 100644 changes/1002.feature.md diff --git a/TODO b/TODO deleted file mode 100644 index f47833fdda..0000000000 --- a/TODO +++ /dev/null @@ -1,21 +0,0 @@ -[ ] modal interaction response (sent to user) - [ ] builder - [x] rest method -[ ] modal interaction event (gotten from user) - [x] all attributes - [x] respond with rest - [ ] respond with restbot -[ ] text input - [x] interaction model - [ ] response builder - [x] builder classes - [ ] helper methods -[x] factory - [x] modal - [x] text input -[ ] tests - [ ] modal interaction response - [ ] rest - [ ] helper methods - [ ] modal interaction event - [ ] entity factory diff --git a/changes/1002.feature.md b/changes/1002.feature.md new file mode 100644 index 0000000000..52e43755f0 --- /dev/null +++ b/changes/1002.feature.md @@ -0,0 +1 @@ +Implement modals diff --git a/hikari/api/rest.py b/hikari/api/rest.py index b0765b9548..cb33427057 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -7418,6 +7418,31 @@ def interaction_message_builder( The interaction message response builder object. """ + @abc.abstractmethod + def interaction_modal_builder( + self, + title: str, + custom_id: str, + *, + components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, + ) -> special_endpoints.InteractionModalBuilder: + """Create a builder for a modal interaction response. + + Parameters + ---------- + title : builtins. + The title that will show up in the modal. + custom_id : builtins. + Developer set custom ID used for identifying interactions with this modal. + components : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] + Sequence of component builders to send in this modal. + + Returns + ------- + hikari.api.special_endpoints.InteractionMessageBuilder + The interaction message response builder object. + """ + @abc.abstractmethod async def fetch_interaction_response( self, application: snowflakes.SnowflakeishOr[guilds.PartialApplication], token: str @@ -7847,7 +7872,7 @@ async def create_modal_response( custom_id : str Developer set custom ID used for identifying interactions with this modal. components : typing.Sequence[special_endpoints.ComponentBuilder] - The components to display in the modal. + Sequence of component builders to send in this modal. """ @abc.abstractmethod diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index b01b0bd922..6ae916f65f 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -41,6 +41,7 @@ "SelectMenuBuilder", "SelectOptionBuilder", "TextInputBuilder", + "InteractionModalBuilder", ] import abc @@ -917,6 +918,107 @@ def set_user_mentions( """ # noqa: E501 - Line too long +class InteractionModalBuilder(InteractionResponseBuilder, abc.ABC): + """Interface of an interaction modal response builder used within REST servers. + + This can be returned by the listener registered to + `hikari.api.interaction_server.InteractionServer` as a response to the interaction + create. + """ + + __slots__: typing.Sequence[str] = () + + @property + @abc.abstractmethod + def type(self) -> typing.Literal[base_interactions.ResponseType.MODAL]: + """Return the type of this response. + + Returns + ------- + hikari.interactions.base_interactions.MessageResponseTypesT + The type of response this is. + """ + + @property + @abc.abstractmethod + def title(self) -> str: + """Return the title that will show up in the modal. + + Returns + ------- + builtins.str + The title that will show up in the modal. + """ + + @property + @abc.abstractmethod + def custom_id(self) -> str: + """Return the developer set custom ID used for identifying interactions with this modal. + + Returns + ------- + builtins.str + Developer set custom ID used for identifying interactions with this modal. + """ + + @property + @abc.abstractmethod + def components(self) -> undefined.UndefinedOr[typing.Sequence[ComponentBuilder]]: + """Return the sequence of component builders to send in this modal. + + Returns + ------ + hikari.undefined.UndefinedOr[typing.Sequence[hikari.api.special_endpoints.ComponentBuilder]] + sequence of component builders to send in this modal. + """ + + @abc.abstractmethod + def set_title(self: _T, title: str, /) -> _T: + """Set the title that will show up in the modal. + + Parameters + ---------- + title : builtins.str + The title that will show up in the modal. + + Returns + ------- + InteractionModalBuilder + Object of this builder. + + """ + + @abc.abstractmethod + def set_custom_id(self: _T, custom_id: str, /) -> _T: + """Set the developer set custom ID used for identifying interactions with this modal. + + Parameters + ---------- + custom_id : builtins.str + The developer set custom ID used for identifying interactions with this modal. + + Returns + ------- + InteractionModalBuilder + Object of this builder. + """ + + @abc.abstractmethod + def add_component(self: _T, component: ComponentBuilder, /) -> _T: + """Add a component to this modal. + + Parameters + ---------- + component : ComponentBuilder + The component builder to add to this modal. + + Returns + ------- + InteractionModalBuilder + Object of this builder. + """ + + class CommandBuilder(abc.ABC): """Interface of a command builder used when bulk creating commands over REST.""" diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 4784a28ab6..45caedb6e2 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -3399,6 +3399,19 @@ def interaction_message_builder( ) -> special_endpoints.InteractionMessageBuilder: return special_endpoints_impl.InteractionMessageBuilder(type=type_) + def interaction_modal_builder( + self, + title: str, + custom_id: str, + *, + components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, + ) -> special_endpoints.InteractionModalBuilder: + return special_endpoints_impl.InteractionModalBuilder( + title=title, + custom_id=custom_id, + components=components, + ) + async def fetch_interaction_response( self, application: snowflakes.SnowflakeishOr[guilds.PartialApplication], token: str ) -> messages_.Message: @@ -3545,7 +3558,7 @@ async def create_modal_response( data = data_binding.JSONObjectBuilder() data.put("title", title) data.put("custom_id", custom_id) - data.put("components", [component.build() for component in components]) + data.put_array("components", components, conversion=lambda component: component.build()) body.put("data", data) diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index ed9282e35d..53488fa02f 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -40,6 +40,7 @@ "LinkButtonBuilder", "SelectMenuBuilder", "TextInputBuilder", + "InteractionModalBuilder", ] import asyncio @@ -86,6 +87,7 @@ _InteractionAutocompleteBuilderT = typing.TypeVar( "_InteractionAutocompleteBuilderT", bound="InteractionAutocompleteBuilder" ) + _InteractionModalBuilderT = typing.TypeVar("_InteractionModalBuilderT", bound="InteractionModalBuilder") _ActionRowBuilderT = typing.TypeVar("_ActionRowBuilderT", bound="ActionRowBuilder") _ButtonBuilderT = typing.TypeVar("_ButtonBuilderT", bound="_ButtonBuilder[typing.Any]") _SelectOptionBuilderT = typing.TypeVar("_SelectOptionBuilderT", bound="_SelectOptionBuilder[typing.Any]") @@ -935,6 +937,54 @@ def build(self, entity_factory: entity_factory_.EntityFactory, /) -> data_bindin return {"type": self._type, "data": data} +@attr.define(kw_only=False, weakref_slot=False) +class InteractionModalBuilder(special_endpoints.InteractionModalBuilder): + """Standard implementation of `hikari.api.special_endpoints.InteractionModalBuilder`.""" + + _title: str = attr.field() + _custom_id: str = attr.field() + _components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = attr.field() + + @property + def type(self) -> typing.Literal[base_interactions.ResponseType.MODAL]: + return base_interactions.ResponseType.MODAL + + @property + def title(self) -> str: + return self._title + + @property + def custom_id(self) -> str: + return self._custom_id + + @property + def components(self) -> undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]]: + return self._components + + def set_title(self: _InteractionModalBuilderT, title: str, /) -> _InteractionModalBuilderT: + self._title = title + return self + + def set_custom_id(self: _InteractionModalBuilderT, custom_id: str, /) -> _InteractionModalBuilderT: + self._custom_id = custom_id + return self + + def add_component( + self: _InteractionModalBuilderT, component: special_endpoints.ComponentBuilder, / + ) -> _InteractionModalBuilderT: + self._component = component + return self + + def build(self, entity_factory: entity_factory_.EntityFactory, /) -> data_binding.JSONObject: + data = data_binding.JSONObjectBuilder() + data.put("title", self._title) + data.put("custom_id", self._custom_id) + data.put_array("components", self._components, conversion=lambda component: component.build()) + + return {"type": self.type, "data": data} + + +@attr_extensions.with_copy @attr.define(kw_only=False, weakref_slot=False) class CommandBuilder(special_endpoints.CommandBuilder): """Standard implementation of `hikari.api.special_endpoints.CommandBuilder`.""" diff --git a/hikari/interactions/command_interactions.py b/hikari/interactions/command_interactions.py index e39f0b0caa..88198c7158 100644 --- a/hikari/interactions/command_interactions.py +++ b/hikari/interactions/command_interactions.py @@ -422,6 +422,31 @@ def build_deferred_response(self) -> special_endpoints.InteractionDeferredBuilde """ return self.app.rest.interaction_deferred_builder(base_interactions.ResponseType.DEFERRED_MESSAGE_CREATE) + def build_modal_response( + self, + title: str, + custom_id: str, + *, + components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, + ) -> special_endpoints.InteractionModalBuilder: + """Create a builder for a modal interaction response. + + Parameters + ---------- + title : builtins.str + The title that will show up in the modal. + custom_id : builtins.str + Developer set custom ID used for identifying interactions with this modal. + components : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] + Sequence of component builders to send in this modal. + + Returns + ------- + hikari.api.special_endpoints.InteractionMessageBuilder + The interaction message response builder object. + """ + return self.app.rest.interaction_modal_builder(title=title, custom_id=custom_id, components=components) + @attr_extensions.with_copy @attr.define(hash=True, kw_only=True, weakref_slot=False) diff --git a/hikari/interactions/component_interactions.py b/hikari/interactions/component_interactions.py index a0e2ffa2f0..c9e8fab86b 100644 --- a/hikari/interactions/component_interactions.py +++ b/hikari/interactions/component_interactions.py @@ -31,6 +31,7 @@ from hikari import channels from hikari import traits +from hikari import undefined from hikari.interactions import base_interactions if typing.TYPE_CHECKING: @@ -209,6 +210,31 @@ def build_deferred_response(self, type_: _DeferredTypesT, /) -> special_endpoint return self.app.rest.interaction_deferred_builder(type_) + def build_modal_response( + self, + title: str, + custom_id: str, + *, + components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, + ) -> special_endpoints.InteractionModalBuilder: + """Create a builder for a modal interaction response. + + Parameters + ---------- + title : builtins.str + The title that will show up in the modal. + custom_id : builtins.str + Developer set custom ID used for identifying interactions with this modal. + components : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] + Sequence of component builders to send in this modal. + + Returns + ------- + hikari.api.special_endpoints.InteractionMessageBuilder + The interaction message response builder object. + """ + return self.app.rest.interaction_modal_builder(title=title, custom_id=custom_id, components=components) + async def fetch_channel(self) -> channels.TextableChannel: """Fetch the channel this interaction occurred in. From 626168c87bd697d2acac8b433bf576593e402143 Mon Sep 17 00:00:00 2001 From: sadru Date: Fri, 4 Feb 2022 20:10:00 +0100 Subject: [PATCH 04/40] Test entity factory modal methods --- hikari/api/special_endpoints.py | 2 +- hikari/impl/entity_factory.py | 9 +++- hikari/interactions/modal_interactions.py | 12 +++-- tests/hikari/impl/test_entity_factory.py | 57 +++++++++++++++++++++++ 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 6ae916f65f..3f9a4ba29e 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -967,7 +967,7 @@ def components(self) -> undefined.UndefinedOr[typing.Sequence[ComponentBuilder]] """Return the sequence of component builders to send in this modal. Returns - ------ + ------- hikari.undefined.UndefinedOr[typing.Sequence[hikari.api.special_endpoints.ComponentBuilder]] sequence of component builders to send in this modal. """ diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index df72cb6782..998c101433 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -2073,6 +2073,10 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod except errors.UnrecognisedEntityError: pass + message: typing.Optional[message_models.Message] = None + if message_payload := payload.get("message"): + message = self.deserialize_message(message_payload) + return modal_interactions.ModalInteraction( app=self._app, application_id=snowflakes.Snowflake(payload["application_id"]), @@ -2088,6 +2092,7 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod version=payload["version"], custom_id=data_payload["custom_id"], components=components, + message=message, ) def deserialize_interaction(self, payload: data_binding.JSONObject) -> base_interactions.PartialInteraction: @@ -2273,7 +2278,9 @@ def deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_m def deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.ModalInteractionTextInput: return modal_interactions.ModalInteractionTextInput( - type=message_models.ComponentType(payload["type"]), custom_id=payload["custom_id"], value=payload["value"] + type=message_models.ComponentType(payload["type"]), + custom_id=payload["custom_id"], + value=payload["value"], ) def deserialize_component(self, payload: data_binding.JSONObject) -> message_models.PartialComponent: diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index ef6fa20f2a..06247aa838 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -84,6 +84,9 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes channel_id: snowflakes.Snowflake = attr.field(eq=False, hash=False, repr=True) """ID of the channel this modal interaction event was triggered in.""" + custom_id: str = attr.field(eq=False, hash=False, repr=True) + """The custom id of the modal.""" + guild_id: typing.Optional[snowflakes.Snowflake] = attr.field(eq=False, hash=False, repr=True) """ID of the guild this modal interaction event was triggered in. @@ -100,6 +103,12 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes for the guild and will otherwise default to `en-US`. """ + message: typing.Optional[messages.Message] = attr.field(eq=False, repr=False) + """The message whose component triggered the modal. + + This will be None if the modal was a response to a command. + """ + member: typing.Optional[base_interactions.InteractionMember] = attr.field(eq=False, hash=False, repr=True) """The member who triggered this modal interaction. @@ -116,9 +125,6 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes locale: str = attr.field(eq=False, hash=False, repr=True) """The selected language of the user who triggered this Modal interaction.""" - custom_id: str = attr.field(eq=False, hash=False, repr=True) - """The custom id of the modal.""" - components: typing.Sequence[ModalInteractionActionRow] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index fab2d7a311..5d432939e1 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -50,6 +50,7 @@ from hikari.interactions import base_interactions from hikari.interactions import command_interactions from hikari.interactions import component_interactions +from hikari.interactions import modal_interactions def test__with_int_cast(): @@ -3514,6 +3515,62 @@ def test_deserialize_component_interaction_with_undefined_fields( assert interaction.guild_locale is None assert isinstance(interaction, component_interactions.ComponentInteraction) + @pytest.fixture() + def modal_interaction_payload(self, interaction_member_payload, message_payload): + # taken from ddocs + return { + "version": 1, + "type": 5, + "token": "unique_interaction_token", + "message": message_payload, + "member": interaction_member_payload, + "id": "846462639134605312", + "guild_id": "290926798626357999", + "data": { + "custom_id": "modaltest", + "components": [ + {"type": 1, "components": [{"value": "Wumpus", "type": 4, "custom_id": "name"}]}, + {"type": 1, "components": [{"value": "Longer Text", "type": 4, "custom_id": "about"}]}, + ], + }, + "channel_id": "345626669114982999", + "application_id": "290926444748734465", + "locale": "en-US", + "guild_locale": "es-ES", + } + + def test_deserialize_modal_interaction( + self, + entity_factory_impl, + mock_app, + modal_interaction_payload, + interaction_member_payload, + message_payload, + ): + interaction = entity_factory_impl.deserialize_modal_interaction(modal_interaction_payload) + assert interaction.app is mock_app + assert interaction.id == 846462639134605312 + assert interaction.application_id == 290926444748734465 + assert interaction.type is base_interactions.InteractionType.MODAL_SUBMIT + assert interaction.token == "unique_interaction_token" + assert interaction.version == 1 + assert interaction.channel_id == 345626669114982999 + assert interaction.guild_id == 290926798626357999 + assert interaction.message == entity_factory_impl.deserialize_message(message_payload) + assert interaction.member == entity_factory_impl._deserialize_interaction_member( + interaction_member_payload, guild_id=290926798626357999 + ) + assert interaction.user is interaction.member.user + assert isinstance(interaction, modal_interactions.ModalInteraction) + + short_action_row = interaction.components[0] + assert isinstance(short_action_row, message_models.ActionRowComponent) + short_text_input = short_action_row.components[0] + assert isinstance(short_text_input, modal_interactions.ModalInteractionTextInput) + assert short_text_input.value == "Wumpus" + assert short_text_input.type == message_models.ComponentType.TEXT_INPUT + assert short_text_input.custom_id == "name" + ################## # STICKER MODELS # ################## From b2364aa363dc3edddf807addff348343f2b2374f Mon Sep 17 00:00:00 2001 From: sadru Date: Wed, 9 Feb 2022 18:05:12 +0100 Subject: [PATCH 05/40] Export modal_interactions --- hikari/impl/special_endpoints.py | 1 - hikari/interactions/__init__.py | 1 + hikari/interactions/__init__.pyi | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 53488fa02f..44ae305c24 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -984,7 +984,6 @@ def build(self, entity_factory: entity_factory_.EntityFactory, /) -> data_bindin return {"type": self.type, "data": data} -@attr_extensions.with_copy @attr.define(kw_only=False, weakref_slot=False) class CommandBuilder(special_endpoints.CommandBuilder): """Standard implementation of `hikari.api.special_endpoints.CommandBuilder`.""" diff --git a/hikari/interactions/__init__.py b/hikari/interactions/__init__.py index 2896af1bc4..214d53a491 100644 --- a/hikari/interactions/__init__.py +++ b/hikari/interactions/__init__.py @@ -26,3 +26,4 @@ from hikari.interactions.base_interactions import * from hikari.interactions.command_interactions import * from hikari.interactions.component_interactions import * +from hikari.interactions.modal_interactions import * diff --git a/hikari/interactions/__init__.pyi b/hikari/interactions/__init__.pyi index 51d1630723..0bba436d86 100644 --- a/hikari/interactions/__init__.pyi +++ b/hikari/interactions/__init__.pyi @@ -4,3 +4,4 @@ from hikari.interactions.base_interactions import * from hikari.interactions.command_interactions import * from hikari.interactions.component_interactions import * +from hikari.interactions.modal_interactions import * From 6726e9a900d068d83137d0fdceb7ad6d4a1a8635 Mon Sep 17 00:00:00 2001 From: sadru Date: Wed, 9 Feb 2022 20:39:10 +0100 Subject: [PATCH 06/40] implement responding with rest bot --- hikari/api/interaction_server.py | 26 +++++++++++++++++++++++++- hikari/impl/interaction_server.py | 28 +++++++++++++++++++++++++++- hikari/impl/rest_bot.py | 28 +++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/hikari/api/interaction_server.py b/hikari/api/interaction_server.py index 4d59902ba8..97f6e545be 100644 --- a/hikari/api/interaction_server.py +++ b/hikari/api/interaction_server.py @@ -33,13 +33,18 @@ from hikari.interactions import base_interactions from hikari.interactions import command_interactions from hikari.interactions import component_interactions + from hikari.interactions import modal_interactions _InteractionT_co = typing.TypeVar("_InteractionT_co", bound=base_interactions.PartialInteraction, covariant=True) _ResponseT_co = typing.TypeVar("_ResponseT_co", bound=special_endpoints.InteractionResponseBuilder, covariant=True) - _MessageResponseBuilderT = typing.Union[ + _ModalMessageResponseBuilderT = typing.Union[ special_endpoints.InteractionDeferredBuilder, special_endpoints.InteractionMessageBuilder, ] + _MessageResponseBuilderT = typing.Union[ + _ModalMessageResponseBuilderT, + special_endpoints.InteractionModalBuilder, + ] ListenerT = typing.Callable[["_InteractionT_co"], typing.Awaitable["_ResponseT_co"]] @@ -156,6 +161,13 @@ def get_listener( ]: ... + @typing.overload + @abc.abstractmethod + def get_listener( + self, interaction_type: typing.Type[modal_interactions.ModalInteraction], / + ) -> typing.Optional[ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT]]: + ... + @typing.overload @abc.abstractmethod def get_listener( @@ -219,6 +231,18 @@ def set_listener( ) -> None: ... + @typing.overload + @abc.abstractmethod + def set_listener( + self, + interaction_type: typing.Type[modal_interactions.ModalInteraction], + listener: typing.Optional[ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT]], + /, + *, + replace: bool = False, + ) -> None: + ... + @abc.abstractmethod def set_listener( self, diff --git a/hikari/impl/interaction_server.py b/hikari/impl/interaction_server.py index 77d1f7dd5f..826c93e6c9 100644 --- a/hikari/impl/interaction_server.py +++ b/hikari/impl/interaction_server.py @@ -53,12 +53,17 @@ from hikari.api import rest as rest_api from hikari.interactions import command_interactions from hikari.interactions import component_interactions + from hikari.interactions import modal_interactions _InteractionT_co = typing.TypeVar("_InteractionT_co", bound=base_interactions.PartialInteraction, covariant=True) - _MessageResponseBuilderT = typing.Union[ + _ModalMessageResponseBuilderT = typing.Union[ special_endpoints.InteractionDeferredBuilder, special_endpoints.InteractionMessageBuilder, ] + _MessageResponseBuilderT = typing.Union[ + _ModalMessageResponseBuilderT, + special_endpoints.InteractionModalBuilder, + ] _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.interaction_server") @@ -534,6 +539,14 @@ def get_listener( ]: ... + @typing.overload + def get_listener( + self, interaction_type: typing.Type[modal_interactions.ModalInteraction], / + ) -> typing.Optional[ + interaction_server.ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT] + ]: + ... + @typing.overload def get_listener( self, interaction_type: typing.Type[_InteractionT_co], / @@ -586,6 +599,19 @@ def set_listener( ) -> None: ... + @typing.overload + def set_listener( + self, + interaction_type: typing.Type[modal_interactions.ModalInteraction], + listener: typing.Optional[ + interaction_server.ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT] + ], + /, + *, + replace: bool = False, + ) -> None: + ... + def set_listener( self, interaction_type: typing.Type[_InteractionT_co], diff --git a/hikari/impl/rest_bot.py b/hikari/impl/rest_bot.py index 4f83bff372..66b591e9d2 100644 --- a/hikari/impl/rest_bot.py +++ b/hikari/impl/rest_bot.py @@ -52,12 +52,17 @@ from hikari.interactions import base_interactions from hikari.interactions import command_interactions from hikari.interactions import component_interactions + from hikari.interactions import modal_interactions _InteractionT_co = typing.TypeVar("_InteractionT_co", bound=base_interactions.PartialInteraction, covariant=True) - _MessageResponseBuilderT = typing.Union[ + _ModalMessageResponseBuilderT = typing.Union[ special_endpoints.InteractionDeferredBuilder, special_endpoints.InteractionMessageBuilder, ] + _MessageResponseBuilderT = typing.Union[ + _ModalMessageResponseBuilderT, + special_endpoints.InteractionModalBuilder, + ] _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.rest_bot") @@ -615,6 +620,14 @@ def get_listener( ]: ... + @typing.overload + def get_listener( + self, interaction_type: typing.Type[modal_interactions.ModalInteraction], / + ) -> typing.Optional[ + interaction_server_.ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT] + ]: + ... + @typing.overload def get_listener( self, interaction_type: typing.Type[_InteractionT_co], / @@ -667,6 +680,19 @@ def set_listener( ) -> None: ... + @typing.overload + def set_listener( + self, + interaction_type: typing.Type[modal_interactions.ModalInteraction], + listener: typing.Optional[ + interaction_server_.ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT] + ], + /, + *, + replace: bool = False, + ) -> None: + ... + def set_listener( self, interaction_type: typing.Type[_InteractionT_co], From 04318d496add1ff54d22cc7f6bd5ab769a75f855 Mon Sep 17 00:00:00 2001 From: sadru Date: Sat, 12 Feb 2022 16:25:07 +0100 Subject: [PATCH 07/40] Add helper methods to ModalInteraction --- hikari/interactions/modal_interactions.py | 162 ++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index 06247aa838..1cbaeaf4a8 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -35,8 +35,11 @@ import attr +from hikari import channels +from hikari import guilds from hikari import messages from hikari import snowflakes +from hikari import traits from hikari.interactions import base_interactions from hikari.internal import attr_extensions @@ -73,6 +76,7 @@ class ModalInteractionActionRow(typing.Protocol): Meant purely for use with ModalInteraction. """ + type: typing.Literal[messages.ComponentType.ACTION_ROW] components: typing.List[ModalInteractionTextInput] @@ -128,6 +132,164 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes components: typing.Sequence[ModalInteractionActionRow] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" + async def fetch_channel(self) -> channels.TextableChannel: + """Fetch the guild channel this was triggered in. + + Returns + ------- + hikari.channels.TextableChannel + The requested partial channel derived object of the channel this was + triggered in. + + Raises + ------ + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.ForbiddenError + If you are missing the `READ_MESSAGES` permission in the channel. + hikari.errors.NotFoundError + If the channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.RateLimitedError + Usually, Hikari will handle and retry on hitting + rate-limits automatically. This includes most bucket-specific + rate-limits and global rate-limits. In some rare edge cases, + however, Discord implements other undocumented rules for + rate-limiting, such as limits per attribute. These cannot be + detected or handled normally by Hikari due to their undocumented + nature, and will trigger this exception if they occur. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + channel = await self.app.rest.fetch_channel(self.channel_id) + assert isinstance(channel, channels.TextableChannel) + return channel + + def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]: + """Get the guild channel this was triggered in from the cache. + + !!! note + This will always return `builtins.None` for interactions triggered + in a DM channel. + + Returns + ------- + typing.Optional[hikari.channels.TextableGuildChannel] + The object of the guild channel that was found in the cache or + `builtins.None`. + """ + if isinstance(self.app, traits.CacheAware): + channel = self.app.cache.get_guild_channel(self.channel_id) + assert isinstance(channel, channels.TextableGuildChannel) + return channel + + return None + + async def fetch_guild(self) -> typing.Optional[guilds.RESTGuild]: + """Fetch the guild this interaction happened in. + + Returns + ------- + typing.Optional[hikari.guilds.RESTGuild] + Object of the guild this interaction happened in or `builtins.None` + if this occurred within a DM channel. + + Raises + ------ + hikari.errors.ForbiddenError + If you are not part of the guild. + hikari.errors.NotFoundError + If the guild is not found. + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.RateLimitedError + Usually, Hikari will handle and retry on hitting + rate-limits automatically. This includes most bucket-specific + rate-limits and global rate-limits. In some rare edge cases, + however, Discord implements other undocumented rules for + rate-limiting, such as limits per attribute. These cannot be + detected or handled normally by Hikari due to their undocumented + nature, and will trigger this exception if they occur. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + if not self.guild_id: + return None + + return await self.app.rest.fetch_guild(self.guild_id) + + def get_guild(self) -> typing.Optional[guilds.GatewayGuild]: + """Get the object of this interaction's guild guild from the cache. + + Returns + ------- + typing.Optional[hikari.guilds.GatewayGuild] + The object of the guild if found, else `builtins.None`. + """ + if self.guild_id and isinstance(self.app, traits.CacheAware): + return self.app.cache.get_guild(self.guild_id) + + return None + + async def fetch_parent_message(self) -> typing.Optional[messages.Message]: + """Fetch the message which this interaction was triggered on. + + Returns + ------- + hikari.messages.Message + The requested message. + + Raises + ------ + builtins.ValueError + If `token` is not available. + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the webhook is not found or the webhook's message wasn't found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.RateLimitedError + Usually, Hikari will handle and retry on hitting + rate-limits automatically. This includes most bucket-specific + rate-limits and global rate-limits. In some rare edge cases, + however, Discord implements other undocumented rules for + rate-limiting, such as limits per attribute. These cannot be + detected or handled normally by Hikari due to their undocumented + nature, and will trigger this exception if they occur. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + if self.message is None: + return None + + return await self.fetch_message(self.message.id) + + def get_parent_message(self) -> typing.Optional[messages.PartialMessage]: + """Get the message which this interaction was triggered on from the cache. + + Returns + ------- + typing.Optional[hikari.messages.Message] + The object of the message found in the cache or `builtins.None`. + """ + if self.message is None: + return None + + if isinstance(self.app, traits.CacheAware): + return self.app.cache.get_message(self.message.id) + + return None + def build_response(self) -> special_endpoints.InteractionMessageBuilder: """Get a message response builder for use in the REST server flow. From 76a685d0b480959978e4ade4fc330d63a83d9676 Mon Sep 17 00:00:00 2001 From: sadru Date: Mon, 14 Feb 2022 09:47:38 +0100 Subject: [PATCH 08/40] Clean up documentation Fixed some issues with wording and copy-pasting. --- hikari/api/rest.py | 10 +++++----- hikari/api/special_endpoints.py | 9 ++++----- hikari/interactions/command_interactions.py | 4 ++-- hikari/interactions/component_interactions.py | 4 ++-- hikari/interactions/modal_interactions.py | 14 +++++++------- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index cb33427057..31cbec0911 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -7430,17 +7430,17 @@ def interaction_modal_builder( Parameters ---------- - title : builtins. + title : builtins.str The title that will show up in the modal. - custom_id : builtins. + custom_id : builtins.str Developer set custom ID used for identifying interactions with this modal. components : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] Sequence of component builders to send in this modal. Returns ------- - hikari.api.special_endpoints.InteractionMessageBuilder - The interaction message response builder object. + hikari.api.special_endpoints.InteractionModalBuilder + The interaction modal response builder object. """ @abc.abstractmethod @@ -7872,7 +7872,7 @@ async def create_modal_response( custom_id : str Developer set custom ID used for identifying interactions with this modal. components : typing.Sequence[special_endpoints.ComponentBuilder] - Sequence of component builders to send in this modal. + A sequence of component builders to send in this modal. """ @abc.abstractmethod diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 3f9a4ba29e..1b6d730ebf 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -969,7 +969,7 @@ def components(self) -> undefined.UndefinedOr[typing.Sequence[ComponentBuilder]] Returns ------- hikari.undefined.UndefinedOr[typing.Sequence[hikari.api.special_endpoints.ComponentBuilder]] - sequence of component builders to send in this modal. + A sequence of component builders to send in this modal. """ @abc.abstractmethod @@ -985,7 +985,6 @@ def set_title(self: _T, title: str, /) -> _T: ------- InteractionModalBuilder Object of this builder. - """ @abc.abstractmethod @@ -1697,12 +1696,12 @@ def label(self) -> str: @property @abc.abstractmethod def placeholder(self) -> undefined.UndefinedOr[str]: - """Return the placeholder text that will disappear when the user types anything. + """Return the placeholder text for when the text input is empty. Returns ------- hikari.undefined.UndefinedOr[builtins.str] - Placeholder text that will disappear when the user types anything. + Placeholder text for when the text input is empty. """ @property @@ -1796,7 +1795,7 @@ def set_label(self: _T, label: str, /) -> _T: @abc.abstractmethod def set_placeholder(self: _T, placeholder: str, /) -> _T: - """Return the placeholder text that will disappear when the user types anything. + """Set the placeholder text for when the text input is empty. Parameters ---------- diff --git a/hikari/interactions/command_interactions.py b/hikari/interactions/command_interactions.py index 88198c7158..01a539942e 100644 --- a/hikari/interactions/command_interactions.py +++ b/hikari/interactions/command_interactions.py @@ -442,8 +442,8 @@ def build_modal_response( Returns ------- - hikari.api.special_endpoints.InteractionMessageBuilder - The interaction message response builder object. + hikari.api.special_endpoints.InteractionModalBuilder + The interaction modal response builder object. """ return self.app.rest.interaction_modal_builder(title=title, custom_id=custom_id, components=components) diff --git a/hikari/interactions/component_interactions.py b/hikari/interactions/component_interactions.py index c9e8fab86b..0929a0ac18 100644 --- a/hikari/interactions/component_interactions.py +++ b/hikari/interactions/component_interactions.py @@ -230,8 +230,8 @@ def build_modal_response( Returns ------- - hikari.api.special_endpoints.InteractionMessageBuilder - The interaction message response builder object. + hikari.api.special_endpoints.InteractionModalBuilder + The interaction modal response builder object. """ return self.app.rest.interaction_modal_builder(title=title, custom_id=custom_id, components=components) diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index 1cbaeaf4a8..18ce1cd044 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -127,19 +127,19 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes """The user who triggered this modal interaction.""" locale: str = attr.field(eq=False, hash=False, repr=True) - """The selected language of the user who triggered this Modal interaction.""" + """The selected language of the user who triggered this modal interaction.""" components: typing.Sequence[ModalInteractionActionRow] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" async def fetch_channel(self) -> channels.TextableChannel: - """Fetch the guild channel this was triggered in. + """Fetch the guild channel this interaction was triggered in. Returns ------- hikari.channels.TextableChannel - The requested partial channel derived object of the channel this was - triggered in. + The requested partial channel derived object of the channel this + interaction was triggered in. Raises ------ @@ -171,7 +171,7 @@ async def fetch_channel(self) -> channels.TextableChannel: return channel def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]: - """Get the guild channel this was triggered in from the cache. + """Get the guild channel this interaction was triggered in from the cache. !!! note This will always return `builtins.None` for interactions triggered @@ -227,7 +227,7 @@ async def fetch_guild(self) -> typing.Optional[guilds.RESTGuild]: return await self.app.rest.fetch_guild(self.guild_id) def get_guild(self) -> typing.Optional[guilds.GatewayGuild]: - """Get the object of this interaction's guild guild from the cache. + """Get the object of the guild this interaction was triggered in from the cache. Returns ------- @@ -280,7 +280,7 @@ def get_parent_message(self) -> typing.Optional[messages.PartialMessage]: Returns ------- typing.Optional[hikari.messages.Message] - The object of the message found in the cache or `builtins.None`. + The object of the message if found or `builtins.None`. """ if self.message is None: return None From f19749e7032f16905d6906ce59438aa9b8acc3b1 Mon Sep 17 00:00:00 2001 From: sadru Date: Tue, 22 Feb 2022 21:53:46 +0100 Subject: [PATCH 09/40] Test TextInput and ModalInteraction --- tests/hikari/impl/test_special_endpoints.py | 98 +++++++++++ .../interactions/test_modal_interactions.py | 162 ++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 tests/hikari/interactions/test_modal_interactions.py diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py index 7afc1bdeb7..ea5cde9145 100644 --- a/tests/hikari/impl/test_special_endpoints.py +++ b/tests/hikari/impl/test_special_endpoints.py @@ -603,6 +603,96 @@ def test_build_partial(self): } +class TestTextInput: + @pytest.fixture() + def text_input(self): + return special_endpoints.TextInputBuilder( + container=mock.Mock(), + style=messages.TextInputStyle.SHORT, + custom_id="o2o2o2", + label="label", + ) + + def test_set_style(self, text_input): + assert text_input.set_style(messages.TextInputStyle.PARAGRAPH) is text_input + assert text_input.style == messages.TextInputStyle.PARAGRAPH + + def test_set_custom_id(self, text_input): + assert text_input.set_custom_id("custooom") is text_input + assert text_input.custom_id == "custooom" + + def test_set_label(self, text_input): + assert text_input.set_label("labeeeel") is text_input + assert text_input.label == "labeeeel" + + def test_set_placeholder(self, text_input): + assert text_input.set_placeholder("place") is text_input + assert text_input.placeholder == "place" + + def test_set_required(self, text_input): + assert text_input.set_required(True) is text_input + assert text_input.required is True + + def test_set_placeholder(self, text_input): + assert text_input.set_value("valueeeee") is text_input + assert text_input.value == "valueeeee" + + def test_set_min_length_(self, text_input): + assert text_input.set_min_length(10) is text_input + assert text_input.min_length == 10 + + def test_set_max_length(self, text_input): + assert text_input.set_max_length(250) is text_input + assert text_input.max_length == 250 + + def test_add_to_container(self, text_input): + assert text_input.add_to_container() is text_input._container + text_input._container.add_component.assert_called_once_with(text_input) + + def test_build(self): + result = special_endpoints.TextInputBuilder( + container=object(), + style=messages.TextInputStyle.SHORT, + custom_id="o2o2o2", + label="label", + ).build() + + assert result == { + "type": messages.ComponentType.TEXT_INPUT, + "style": 1, + "custom_id": "o2o2o2", + "label": "label", + } + + def test_build_partial(self): + result = ( + special_endpoints.TextInputBuilder( + container=object(), + style=messages.TextInputStyle.SHORT, + custom_id="o2o2o2", + label="label", + ) + .set_placeholder("placeholder") + .set_value("value") + .set_required(False) + .set_min_length(10) + .set_max_length(250) + .build() + ) + + assert result == { + "type": messages.ComponentType.TEXT_INPUT, + "style": 1, + "custom_id": "o2o2o2", + "label": "label", + "placeholder": "placeholder", + "value": "value", + "required": False, + "min_length": 10, + "max_length": 250, + } + + class TestActionRowBuilder: def test_components_property(self): mock_component = object() @@ -633,6 +723,14 @@ def test_add_select_menu(self): assert row.components == [menu] + def test_add_text_input(self): + row = special_endpoints.ActionRowBuilder() + menu = row.add_text_input(1, "hihihi", "label") + + menu.add_to_container() + + assert row.components == [menu] + def test_build(self): mock_component_1 = mock.Mock() mock_component_2 = mock.Mock() diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py new file mode 100644 index 0000000000..b7f04782ad --- /dev/null +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import mock +import pytest + +from hikari import channels +from hikari import messages +from hikari import snowflakes +from hikari import traits +from hikari.impl import special_endpoints +from hikari.interactions import base_interactions +from hikari.interactions import modal_interactions +from tests.hikari import hikari_test_helpers + + +@pytest.fixture() +def mock_app(): + return mock.Mock(rest=mock.AsyncMock()) + + +class TestModalInteraction: + @pytest.fixture() + def mock_modal_interaction(self, mock_app): + return modal_interactions.ModalInteraction( + app=mock_app, + id=snowflakes.Snowflake(2312312), + type=base_interactions.InteractionType.APPLICATION_COMMAND, + channel_id=snowflakes.Snowflake(3123123), + guild_id=snowflakes.Snowflake(5412231), + member=object(), + user=object(), + token="httptptptptptptptp", + version=1, + application_id=snowflakes.Snowflake(43123), + custom_id="OKOKOK", + message=object(), + locale="es-ES", + guild_locale="en-US", + components=special_endpoints.ActionRowBuilder( + components=[ + modal_interactions.ModalInteractionTextInput( + type=messages.ComponentType.TEXT_INPUT, custom_id="le id", value="le value" + ) + ], + ), + ) + + def test_build_response(self, mock_modal_interaction, mock_app): + mock_app.rest.interaction_message_builder = mock.Mock() + response = mock_modal_interaction.build_response() + + assert response is mock_app.rest.interaction_message_builder.return_value + mock_app.rest.interaction_message_builder.assert_called_once() + + def test_build_deferred_response(self, mock_modal_interaction, mock_app): + mock_app.rest.interaction_deferred_builder = mock.Mock() + response = mock_modal_interaction.build_deferred_response() + + assert response is mock_app.rest.interaction_deferred_builder.return_value + mock_app.rest.interaction_deferred_builder.assert_called_once() + + @pytest.mark.asyncio() + async def test_fetch_channel(self, mock_modal_interaction, mock_app): + mock_app.rest.fetch_channel.return_value = mock.Mock(channels.TextableChannel) + + assert await mock_modal_interaction.fetch_channel() is mock_app.rest.fetch_channel.return_value + + mock_app.rest.fetch_channel.assert_awaited_once_with(3123123) + + def test_get_channel(self, mock_modal_interaction, mock_app): + mock_app.cache.get_guild_channel.return_value = mock.Mock(channels.GuildTextChannel) + + assert mock_modal_interaction.get_channel() is mock_app.cache.get_guild_channel.return_value + + mock_app.cache.get_guild_channel.assert_called_once_with(3123123) + + def test_get_channel_without_cache(self, mock_modal_interaction): + mock_modal_interaction.app = mock.Mock(traits.RESTAware) + + assert mock_modal_interaction.get_channel() is None + + @pytest.mark.asyncio() + async def test_fetch_guild(self, mock_modal_interaction, mock_app): + mock_modal_interaction.guild_id = 43123123 + + assert await mock_modal_interaction.fetch_guild() is mock_app.rest.fetch_guild.return_value + + mock_app.rest.fetch_guild.assert_awaited_once_with(43123123) + + @pytest.mark.asyncio() + async def test_fetch_guild_for_dm_interaction(self, mock_modal_interaction, mock_app): + mock_modal_interaction.guild_id = None + + assert await mock_modal_interaction.fetch_guild() is None + + mock_app.rest.fetch_guild.assert_not_called() + + def test_get_guild(self, mock_modal_interaction, mock_app): + mock_modal_interaction.guild_id = 874356 + + assert mock_modal_interaction.get_guild() is mock_app.cache.get_guild.return_value + + mock_app.cache.get_guild.assert_called_once_with(874356) + + def test_get_guild_for_dm_interaction(self, mock_modal_interaction, mock_app): + mock_modal_interaction.guild_id = None + + assert mock_modal_interaction.get_guild() is None + + mock_app.cache.get_guild.assert_not_called() + + def test_get_guild_when_cacheless(self, mock_modal_interaction, mock_app): + mock_modal_interaction.guild_id = 321123 + mock_modal_interaction.app = mock.Mock(traits.RESTAware) + + assert mock_modal_interaction.get_guild() is None + + mock_app.cache.get_guild.assert_not_called() + + @pytest.mark.asyncio() + async def test_fetch_parent_message(self): + stub_interaction = hikari_test_helpers.mock_class_namespace( + modal_interactions.ModalInteraction, fetch_message=mock.AsyncMock(), init_=False + )() + stub_interaction.message = mock.Mock(id=3421) + + assert await stub_interaction.fetch_parent_message() is stub_interaction.fetch_message.return_value + + stub_interaction.fetch_message.assert_awaited_once_with(3421) + + def test_get_parent_message(self, mock_modal_interaction, mock_app): + mock_modal_interaction.message = mock.Mock(id=321655) + + assert mock_modal_interaction.get_parent_message() is mock_app.cache.get_message.return_value + + mock_app.cache.get_message.assert_called_once_with(321655) + + def test_get_parent_message_when_cacheless(self, mock_modal_interaction, mock_app): + mock_modal_interaction.app = mock.Mock(traits.RESTAware) + + assert mock_modal_interaction.get_parent_message() is None + + mock_app.cache.get_message.assert_not_called() From cf0faa31d96e0e618570b0f3582c6885a2a34cd2 Mon Sep 17 00:00:00 2001 From: sadru Date: Tue, 22 Feb 2022 22:37:38 +0100 Subject: [PATCH 10/40] Test rest modal response --- tests/hikari/impl/test_rest.py | 21 +++++++++++++++++++ tests/hikari/impl/test_special_endpoints.py | 2 +- .../interactions/test_base_interactions.py | 12 +++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index ed9f269aa8..9bec5bc96a 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -1379,6 +1379,13 @@ def test_interaction_message_builder(self, rest_client): assert result.type == 4 assert isinstance(result, special_endpoints.InteractionMessageBuilder) + def test_interaction_modal_builder(self, rest_client): + result = rest_client.interaction_modal_builder("title", "custom") + result.add_component(special_endpoints.ActionRowBuilder().add_text_input(1, "idd", "labell").add_to_container()) + + assert result.type == 9 + assert isinstance(result, special_endpoints.InteractionModalBuilder) + @pytest.mark.asyncio() class TestRESTClientImplAsync: @@ -5076,3 +5083,17 @@ async def test_create_autocomplete_response(self, rest_client): json={"type": 8, "data": {"choices": [{"name": "a", "value": "b"}, {"name": "foo", "value": "bar"}]}}, no_auth=True, ) + + async def test_create_modal_response(self, rest_client): + expected_route = routes.POST_INTERACTION_RESPONSE.compile(interaction=1235431, token="dissssnake") + rest_client._request = mock.AsyncMock() + + await rest_client.create_modal_response( + StubModel(1235431), "dissssnake", title="title", custom_id="idd", components=[] + ) + + rest_client._request.assert_awaited_once_with( + expected_route, + json={"type": 9, "data": {"title": "title", "custom_id": "idd", "components": []}}, + no_auth=True, + ) diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py index ea5cde9145..c805e01893 100644 --- a/tests/hikari/impl/test_special_endpoints.py +++ b/tests/hikari/impl/test_special_endpoints.py @@ -633,7 +633,7 @@ def test_set_required(self, text_input): assert text_input.set_required(True) is text_input assert text_input.required is True - def test_set_placeholder(self, text_input): + def test_set_value(self, text_input): assert text_input.set_value("valueeeee") is text_input assert text_input.value == "valueeeee" diff --git a/tests/hikari/interactions/test_base_interactions.py b/tests/hikari/interactions/test_base_interactions.py index 16f928627e..7467ff812a 100644 --- a/tests/hikari/interactions/test_base_interactions.py +++ b/tests/hikari/interactions/test_base_interactions.py @@ -134,6 +134,18 @@ async def test_create_initial_response_without_optional_args(self, mock_message_ role_mentions=undefined.UNDEFINED, ) + @pytest.mark.asyncio() + async def test_create_modal_response(self, mock_message_response_mixin, mock_app): + await mock_message_response_mixin.create_modal_response("title", "custom_id", []) + + mock_app.rest.create_modal_response.assert_awaited_once_with( + 34123, + "399393939doodsodso", + title="title", + custom_id="custom_id", + components=[], + ) + @pytest.mark.asyncio() async def test_edit_initial_response_with_optional_args(self, mock_message_response_mixin, mock_app): mock_embed_1 = object() From cb699297a43562472d0dc5053b1ad9f6d2fee80f Mon Sep 17 00:00:00 2001 From: sadru Date: Fri, 25 Feb 2022 21:20:50 +0100 Subject: [PATCH 11/40] Give style a default value --- hikari/api/special_endpoints.py | 25 +++++++++------------ hikari/impl/special_endpoints.py | 13 +++++------ tests/hikari/impl/test_rest.py | 2 +- tests/hikari/impl/test_special_endpoints.py | 5 +---- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 1b6d730ebf..6f267c3feb 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -1656,17 +1656,6 @@ class TextInputBuilder(ComponentBuilder, abc.ABC, typing.Generic[_ContainerT]): __slots__: typing.Sequence[str] = () - @property - @abc.abstractmethod - def style(self) -> messages.TextInputStyle: - """Style to use for the text input. - - Returns - ------- - builtins.str - Style to use for the text input. - """ - @property @abc.abstractmethod def custom_id(self) -> str: @@ -1693,6 +1682,17 @@ def label(self) -> str: Label above this text input. """ + @property + @abc.abstractmethod + def style(self) -> messages.TextInputStyle: + """Style to use for the text input. + + Returns + ------- + builtins.str + Style to use for the text input. + """ + @property @abc.abstractmethod def placeholder(self) -> undefined.UndefinedOr[str]: @@ -1984,7 +1984,6 @@ def add_select_menu(self: _T, custom_id: str, /) -> SelectMenuBuilder[_T]: @abc.abstractmethod def add_text_input( self: _T, - style: typing.Union[messages.TextInputStyle, int], custom_id: str, label: str, ) -> TextInputBuilder[_T]: @@ -1992,8 +1991,6 @@ def add_text_input( Parameters ---------- - style : typing.Union[hikari.messages.TextInputStyle, int] - Style to use for the text input. custom_id : builtins.str Developer set custom ID used for identifying this text input. label : builtins.str diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 44ae305c24..40b6044b8c 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -1353,20 +1353,16 @@ class TextInputBuilder(special_endpoints.TextInputBuilder[_ContainerProtoT]): """Standard implementation of `hikari.api.special_endpoints.TextInputBuilder`.""" _container: _ContainerProtoT = attr.field() - _style: messages.TextInputStyle = attr.field() _custom_id: str = attr.field() _label: str = attr.field() + _style: messages.TextInputStyle = attr.field(default=messages.TextInputStyle.SHORT) _placeholder: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED, kw_only=True) _value: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED, kw_only=True) _required: undefined.UndefinedOr[bool] = attr.field(default=undefined.UNDEFINED, kw_only=True) _min_length: undefined.UndefinedOr[int] = attr.field(default=undefined.UNDEFINED, kw_only=True) _max_length: undefined.UndefinedOr[int] = attr.field(default=undefined.UNDEFINED, kw_only=True) - @property - def style(self) -> messages.TextInputStyle: - return self._style - @property def custom_id(self) -> str: return self._custom_id @@ -1375,6 +1371,10 @@ def custom_id(self) -> str: def label(self) -> str: return self._label + @property + def style(self) -> messages.TextInputStyle: + return self._style + @property def placeholder(self) -> undefined.UndefinedOr[str]: return self._placeholder @@ -1514,12 +1514,11 @@ def add_select_menu( def add_text_input( self: _ActionRowBuilderT, - style: typing.Union[messages.TextInputStyle, int], custom_id: str, label: str, ) -> special_endpoints.TextInputBuilder[_ActionRowBuilderT]: self._assert_can_add_type(messages.ComponentType.TEXT_INPUT) - return TextInputBuilder(container=self, style=messages.TextInputStyle(style), custom_id=custom_id, label=label) + return TextInputBuilder(container=self, custom_id=custom_id, label=label) def build(self) -> data_binding.JSONObject: return { diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 9bec5bc96a..97d4180ea5 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -1381,7 +1381,7 @@ def test_interaction_message_builder(self, rest_client): def test_interaction_modal_builder(self, rest_client): result = rest_client.interaction_modal_builder("title", "custom") - result.add_component(special_endpoints.ActionRowBuilder().add_text_input(1, "idd", "labell").add_to_container()) + result.add_component(special_endpoints.ActionRowBuilder().add_text_input("idd", "labell").add_to_container()) assert result.type == 9 assert isinstance(result, special_endpoints.InteractionModalBuilder) diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py index c805e01893..ff4c70381b 100644 --- a/tests/hikari/impl/test_special_endpoints.py +++ b/tests/hikari/impl/test_special_endpoints.py @@ -608,7 +608,6 @@ class TestTextInput: def text_input(self): return special_endpoints.TextInputBuilder( container=mock.Mock(), - style=messages.TextInputStyle.SHORT, custom_id="o2o2o2", label="label", ) @@ -652,7 +651,6 @@ def test_add_to_container(self, text_input): def test_build(self): result = special_endpoints.TextInputBuilder( container=object(), - style=messages.TextInputStyle.SHORT, custom_id="o2o2o2", label="label", ).build() @@ -668,7 +666,6 @@ def test_build_partial(self): result = ( special_endpoints.TextInputBuilder( container=object(), - style=messages.TextInputStyle.SHORT, custom_id="o2o2o2", label="label", ) @@ -725,7 +722,7 @@ def test_add_select_menu(self): def test_add_text_input(self): row = special_endpoints.ActionRowBuilder() - menu = row.add_text_input(1, "hihihi", "label") + menu = row.add_text_input("hihihi", "label") menu.add_to_container() From 6e3add7de6ea0a064200df76346712c33086a622 Mon Sep 17 00:00:00 2001 From: sadru Date: Sat, 5 Mar 2022 17:27:35 +0100 Subject: [PATCH 12/40] Resolve some suggestions --- hikari/api/entity_factory.py | 31 +++++++++++++++++++ hikari/impl/entity_factory.py | 4 +-- hikari/impl/special_endpoints.py | 1 - hikari/interactions/modal_interactions.py | 11 +++++-- hikari/messages.py | 7 ++--- tests/hikari/impl/test_entity_factory.py | 2 +- .../interactions/test_modal_interactions.py | 2 +- 7 files changed, 45 insertions(+), 13 deletions(-) diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 87c0b18af1..ab3fbdfb9d 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -55,6 +55,7 @@ from hikari.interactions import base_interactions from hikari.interactions import command_interactions from hikari.interactions import component_interactions + from hikari.interactions import modal_interactions from hikari.internal import data_binding @@ -1055,6 +1056,21 @@ def deserialize_command_interaction( The deserialized command interaction object. """ + @abc.abstractmethod + def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> modal_interactions.ModalInteraction: + """Parse a raw payload from Discord into a modal interaction object. + + Parameters + ---------- + payload : hikari.internal.data_binding.JSONObject + The JSON payload to deserialize. + + Returns + ------- + hikari.interactions.modal_interactions.ModalInteraction + The deserialized modal interaction object. + """ + @abc.abstractmethod def deserialize_interaction(self, payload: data_binding.JSONObject) -> base_interactions.PartialInteraction: """Parse a raw payload from Discord into a interaction object. @@ -1274,6 +1290,21 @@ def deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_m The deserialized button component. """ + @abc.abstractmethod + def deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.PartialTextInput: + """Parse a raw payload from Discord into a text input component object. + + Parameters + ---------- + payload : hikari.internal.data_binding.JSONObject + The JSON payload to deserialize. + + Returns + ------- + hikari.interactions.modal_interactions.PartialTextInput + The deserialized text input component. + """ + @abc.abstractmethod def deserialize_component(self, payload: data_binding.JSONObject) -> message_models.PartialComponent: """Parse a raw payload from Discord into a message component object. diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 998c101433..f228c7f08e 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -2276,8 +2276,8 @@ def deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_m is_disabled=payload.get("disabled", False), ) - def deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.ModalInteractionTextInput: - return modal_interactions.ModalInteractionTextInput( + def deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.PartialTextInput: + return modal_interactions.PartialTextInput( type=message_models.ComponentType(payload["type"]), custom_id=payload["custom_id"], value=payload["value"], diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 40b6044b8c..449067287d 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -972,7 +972,6 @@ def set_custom_id(self: _InteractionModalBuilderT, custom_id: str, /) -> _Intera def add_component( self: _InteractionModalBuilderT, component: special_endpoints.ComponentBuilder, / ) -> _InteractionModalBuilderT: - self._component = component return self def build(self, entity_factory: entity_factory_.EntityFactory, /) -> data_binding.JSONObject: diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index 18ce1cd044..555e8e46e6 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -27,7 +27,7 @@ __all__: typing.List[str] = [ "ModalResponseTypesT", "ModalInteraction", - "ModalInteractionTextInput", + "PartialTextInput", "ModalInteraction", ] @@ -60,7 +60,7 @@ @attr.define(kw_only=True, weakref_slot=False) -class ModalInteractionTextInput(messages.PartialComponent): +class PartialTextInput(messages.PartialComponent): """A text input component in a modal interaction.""" custom_id: str = attr.field(repr=True) @@ -76,8 +76,13 @@ class ModalInteractionActionRow(typing.Protocol): Meant purely for use with ModalInteraction. """ + __slots__ = () + type: typing.Literal[messages.ComponentType.ACTION_ROW] - components: typing.List[ModalInteractionTextInput] + """The type of component this is.""" + + components: typing.List[PartialTextInput] + """Sequence of the components contained within this row.""" @attr_extensions.with_copy diff --git a/hikari/messages.py b/hikari/messages.py index 398d6ed104..aa4be5fe81 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -530,11 +530,7 @@ class ComponentType(int, enums.Enum): """A text input component !! note - This component may only be used in modals. - - !!! note - This cannot be top-level and must be within a container component such - as `ComponentType.ACTION_ROW`. + This component may only be used inside a modal container. """ @@ -606,6 +602,7 @@ class TextInputStyle(int, enums.Enum): SHORT = 1 """Intended for short single-line text.""" + PARAGRAPH = 2 """Intended for much longer inputs.""" diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 5d432939e1..17d731c046 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -3566,7 +3566,7 @@ def test_deserialize_modal_interaction( short_action_row = interaction.components[0] assert isinstance(short_action_row, message_models.ActionRowComponent) short_text_input = short_action_row.components[0] - assert isinstance(short_text_input, modal_interactions.ModalInteractionTextInput) + assert isinstance(short_text_input, modal_interactions.PartialTextInput) assert short_text_input.value == "Wumpus" assert short_text_input.type == message_models.ComponentType.TEXT_INPUT assert short_text_input.custom_id == "name" diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py index b7f04782ad..4626778449 100644 --- a/tests/hikari/interactions/test_modal_interactions.py +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -57,7 +57,7 @@ def mock_modal_interaction(self, mock_app): guild_locale="en-US", components=special_endpoints.ActionRowBuilder( components=[ - modal_interactions.ModalInteractionTextInput( + modal_interactions.PartialTextInput( type=messages.ComponentType.TEXT_INPUT, custom_id="le id", value="le value" ) ], From 8c9e335cde0d3829d46deb542cdbbc4e16fee452 Mon Sep 17 00:00:00 2001 From: sadru Date: Sun, 13 Mar 2022 21:52:41 +0100 Subject: [PATCH 13/40] Export modal interactions I love having to deal with random changes in unrelated PRs. --- hikari/__init__.py | 1 + hikari/__init__.pyi | 1 + 2 files changed, 2 insertions(+) diff --git a/hikari/__init__.py b/hikari/__init__.py index 99a5c52d35..5fd3e57fd5 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -102,6 +102,7 @@ from hikari.interactions.base_interactions import * from hikari.interactions.command_interactions import * from hikari.interactions.component_interactions import * +from hikari.interactions.modal_interactions import * from hikari.invites import * from hikari.iterators import * from hikari.messages import * diff --git a/hikari/__init__.pyi b/hikari/__init__.pyi index e02f17abf9..ca618fbc97 100644 --- a/hikari/__init__.pyi +++ b/hikari/__init__.pyi @@ -75,6 +75,7 @@ from hikari.intents import * from hikari.interactions.base_interactions import * from hikari.interactions.command_interactions import * from hikari.interactions.component_interactions import * +from hikari.interactions.modal_interactions import * from hikari.invites import * from hikari.iterators import * from hikari.messages import * From fedbdb7743586e76ce0cab04c26af90d231dd97b Mon Sep 17 00:00:00 2001 From: davfsa Date: Fri, 25 Mar 2022 21:59:13 +0100 Subject: [PATCH 14/40] Temporarily freeze jinja2 to 3.0.3 (#1098) --- flake8-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/flake8-requirements.txt b/flake8-requirements.txt index d4f379deac..eb1d917c10 100644 --- a/flake8-requirements.txt +++ b/flake8-requirements.txt @@ -14,6 +14,7 @@ flake8-docstrings==1.6.0 # pydocstyle support flake8-executable==2.1.1 # shebangs flake8-fixme==1.1.1 # "fix me" counter flake8-functions==0.0.7 # function linting +jinja2==3.0.3 # temporarily freeze jinja2 due to incompatibilities with flake8-html flake8-html==0.4.1 # html output flake8-if-statements==0.1.0 # condition linting flake8-isort==4.1.1 # runs isort From 80171ccb12cda9bcc2552b6706bfa83d974dfbce Mon Sep 17 00:00:00 2001 From: sadru Date: Fri, 25 Mar 2022 22:25:09 +0100 Subject: [PATCH 15/40] Remove useless import --- hikari/interactions/component_interactions.py | 1 - tests/hikari/interactions/test_base_interactions.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/hikari/interactions/component_interactions.py b/hikari/interactions/component_interactions.py index 638ae30eb0..7646f8529c 100644 --- a/hikari/interactions/component_interactions.py +++ b/hikari/interactions/component_interactions.py @@ -31,7 +31,6 @@ from hikari import channels from hikari import traits -from hikari import undefined from hikari.interactions import base_interactions if typing.TYPE_CHECKING: diff --git a/tests/hikari/interactions/test_base_interactions.py b/tests/hikari/interactions/test_base_interactions.py index 0c3893c484..6c95f5d987 100644 --- a/tests/hikari/interactions/test_base_interactions.py +++ b/tests/hikari/interactions/test_base_interactions.py @@ -202,7 +202,7 @@ async def test_delete_initial_response(self, mock_message_response_mixin, mock_a class TestModalResponseMixin: - @pytest.fixture + @pytest.fixture() def mock_modal_response_mixin(self, mock_app): return base_interactions.ModalResponseMixin( app=mock_app, From 42c1d5f1a20716c4fbfb9b4d6645c3b69592aeb9 Mon Sep 17 00:00:00 2001 From: sadru Date: Fri, 25 Mar 2022 22:59:17 +0100 Subject: [PATCH 16/40] Improve naming --- hikari/api/interaction_server.py | 18 +++++++-------- hikari/impl/entity_factory.py | 4 ++-- hikari/impl/interaction_server.py | 22 ++++++++----------- hikari/impl/rest_bot.py | 20 ++++++++--------- hikari/interactions/modal_interactions.py | 6 ++--- tests/hikari/impl/test_entity_factory.py | 2 +- .../interactions/test_modal_interactions.py | 2 +- 7 files changed, 34 insertions(+), 40 deletions(-) diff --git a/hikari/api/interaction_server.py b/hikari/api/interaction_server.py index 97f6e545be..09b227d05b 100644 --- a/hikari/api/interaction_server.py +++ b/hikari/api/interaction_server.py @@ -37,12 +37,12 @@ _InteractionT_co = typing.TypeVar("_InteractionT_co", bound=base_interactions.PartialInteraction, covariant=True) _ResponseT_co = typing.TypeVar("_ResponseT_co", bound=special_endpoints.InteractionResponseBuilder, covariant=True) - _ModalMessageResponseBuilderT = typing.Union[ + _MessageResponseBuilderT = typing.Union[ special_endpoints.InteractionDeferredBuilder, special_endpoints.InteractionMessageBuilder, ] - _MessageResponseBuilderT = typing.Union[ - _ModalMessageResponseBuilderT, + _ModalResponseBuilder = typing.Union[ + _MessageResponseBuilderT, special_endpoints.InteractionModalBuilder, ] @@ -142,14 +142,14 @@ async def on_interaction(self, body: bytes, signature: bytes, timestamp: bytes) @abc.abstractmethod def get_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], / - ) -> typing.Optional[ListenerT[command_interactions.CommandInteraction, _MessageResponseBuilderT]]: + ) -> typing.Optional[ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilder]]: ... @typing.overload @abc.abstractmethod def get_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], / - ) -> typing.Optional[ListenerT[component_interactions.ComponentInteraction, _MessageResponseBuilderT]]: + ) -> typing.Optional[ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilder]]: ... @typing.overload @@ -165,7 +165,7 @@ def get_listener( @abc.abstractmethod def get_listener( self, interaction_type: typing.Type[modal_interactions.ModalInteraction], / - ) -> typing.Optional[ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT]]: + ) -> typing.Optional[ListenerT[modal_interactions.ModalInteraction, _MessageResponseBuilderT]]: ... @typing.overload @@ -198,7 +198,7 @@ def get_listener( def set_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], - listener: typing.Optional[ListenerT[command_interactions.CommandInteraction, _MessageResponseBuilderT]], + listener: typing.Optional[ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilder]], /, *, replace: bool = False, @@ -210,7 +210,7 @@ def set_listener( def set_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], - listener: typing.Optional[ListenerT[component_interactions.ComponentInteraction, _MessageResponseBuilderT]], + listener: typing.Optional[ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilder]], /, *, replace: bool = False, @@ -236,7 +236,7 @@ def set_listener( def set_listener( self, interaction_type: typing.Type[modal_interactions.ModalInteraction], - listener: typing.Optional[ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT]], + listener: typing.Optional[ListenerT[modal_interactions.ModalInteraction, _MessageResponseBuilderT]], /, *, replace: bool = False, diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index a2746fd6ef..11d0170b60 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -2285,8 +2285,8 @@ def _deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_ is_disabled=payload.get("disabled", False), ) - def _deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.PartialTextInput: - return modal_interactions.PartialTextInput( + def _deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.InteractionTextInput: + return modal_interactions.InteractionTextInput( type=message_models.ComponentType(payload["type"]), custom_id=payload["custom_id"], value=payload["value"], diff --git a/hikari/impl/interaction_server.py b/hikari/impl/interaction_server.py index 826c93e6c9..908732c460 100644 --- a/hikari/impl/interaction_server.py +++ b/hikari/impl/interaction_server.py @@ -56,12 +56,12 @@ from hikari.interactions import modal_interactions _InteractionT_co = typing.TypeVar("_InteractionT_co", bound=base_interactions.PartialInteraction, covariant=True) - _ModalMessageResponseBuilderT = typing.Union[ + _MessageResponseBuilderT = typing.Union[ special_endpoints.InteractionDeferredBuilder, special_endpoints.InteractionMessageBuilder, ] - _MessageResponseBuilderT = typing.Union[ - _ModalMessageResponseBuilderT, + _ModalResponseBuilderT = typing.Union[ + _MessageResponseBuilderT, special_endpoints.InteractionModalBuilder, ] @@ -516,16 +516,14 @@ async def start( @typing.overload def get_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], / - ) -> typing.Optional[ - interaction_server.ListenerT[command_interactions.CommandInteraction, _MessageResponseBuilderT] - ]: + ) -> typing.Optional[interaction_server.ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilderT]]: ... @typing.overload def get_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], / ) -> typing.Optional[ - interaction_server.ListenerT[component_interactions.ComponentInteraction, _MessageResponseBuilderT] + interaction_server.ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilderT] ]: ... @@ -542,9 +540,7 @@ def get_listener( @typing.overload def get_listener( self, interaction_type: typing.Type[modal_interactions.ModalInteraction], / - ) -> typing.Optional[ - interaction_server.ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT] - ]: + ) -> typing.Optional[interaction_server.ListenerT[modal_interactions.ModalInteraction, _MessageResponseBuilderT]]: ... @typing.overload @@ -563,7 +559,7 @@ def set_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], listener: typing.Optional[ - interaction_server.ListenerT[command_interactions.CommandInteraction, _MessageResponseBuilderT] + interaction_server.ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilderT] ], /, *, @@ -576,7 +572,7 @@ def set_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], listener: typing.Optional[ - interaction_server.ListenerT[component_interactions.ComponentInteraction, _MessageResponseBuilderT] + interaction_server.ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilderT] ], /, *, @@ -604,7 +600,7 @@ def set_listener( self, interaction_type: typing.Type[modal_interactions.ModalInteraction], listener: typing.Optional[ - interaction_server.ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT] + interaction_server.ListenerT[modal_interactions.ModalInteraction, _MessageResponseBuilderT] ], /, *, diff --git a/hikari/impl/rest_bot.py b/hikari/impl/rest_bot.py index 5c8cd5689e..08249d65ec 100644 --- a/hikari/impl/rest_bot.py +++ b/hikari/impl/rest_bot.py @@ -55,12 +55,12 @@ from hikari.interactions import modal_interactions _InteractionT_co = typing.TypeVar("_InteractionT_co", bound=base_interactions.PartialInteraction, covariant=True) - _ModalMessageResponseBuilderT = typing.Union[ + _MessageResponseBuilderT = typing.Union[ special_endpoints.InteractionDeferredBuilder, special_endpoints.InteractionMessageBuilder, ] - _MessageResponseBuilderT = typing.Union[ - _ModalMessageResponseBuilderT, + _ModalResponseBuilderT = typing.Union[ + _MessageResponseBuilderT, special_endpoints.InteractionModalBuilder, ] @@ -598,7 +598,7 @@ async def start( def get_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], / ) -> typing.Optional[ - interaction_server_.ListenerT[command_interactions.CommandInteraction, _MessageResponseBuilderT] + interaction_server_.ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilderT] ]: ... @@ -606,7 +606,7 @@ def get_listener( def get_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], / ) -> typing.Optional[ - interaction_server_.ListenerT[component_interactions.ComponentInteraction, _MessageResponseBuilderT] + interaction_server_.ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilderT] ]: ... @@ -623,9 +623,7 @@ def get_listener( @typing.overload def get_listener( self, interaction_type: typing.Type[modal_interactions.ModalInteraction], / - ) -> typing.Optional[ - interaction_server_.ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT] - ]: + ) -> typing.Optional[interaction_server_.ListenerT[modal_interactions.ModalInteraction, _MessageResponseBuilderT]]: ... @typing.overload @@ -644,7 +642,7 @@ def set_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], listener: typing.Optional[ - interaction_server_.ListenerT[command_interactions.CommandInteraction, _MessageResponseBuilderT] + interaction_server_.ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilderT] ], /, *, @@ -657,7 +655,7 @@ def set_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], listener: typing.Optional[ - interaction_server_.ListenerT[component_interactions.ComponentInteraction, _MessageResponseBuilderT] + interaction_server_.ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilderT] ], /, *, @@ -685,7 +683,7 @@ def set_listener( self, interaction_type: typing.Type[modal_interactions.ModalInteraction], listener: typing.Optional[ - interaction_server_.ListenerT[modal_interactions.ModalInteraction, _ModalMessageResponseBuilderT] + interaction_server_.ListenerT[modal_interactions.ModalInteraction, _MessageResponseBuilderT] ], /, *, diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index 555e8e46e6..90779203e8 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -27,7 +27,7 @@ __all__: typing.List[str] = [ "ModalResponseTypesT", "ModalInteraction", - "PartialTextInput", + "InteractionTextInput", "ModalInteraction", ] @@ -60,7 +60,7 @@ @attr.define(kw_only=True, weakref_slot=False) -class PartialTextInput(messages.PartialComponent): +class InteractionTextInput(messages.PartialComponent): """A text input component in a modal interaction.""" custom_id: str = attr.field(repr=True) @@ -81,7 +81,7 @@ class ModalInteractionActionRow(typing.Protocol): type: typing.Literal[messages.ComponentType.ACTION_ROW] """The type of component this is.""" - components: typing.List[PartialTextInput] + components: typing.List[InteractionTextInput] """Sequence of the components contained within this row.""" diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 2260de1029..c3edefd739 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -3577,7 +3577,7 @@ def test_deserialize_modal_interaction( short_action_row = interaction.components[0] assert isinstance(short_action_row, message_models.ActionRowComponent) short_text_input = short_action_row.components[0] - assert isinstance(short_text_input, modal_interactions.PartialTextInput) + assert isinstance(short_text_input, modal_interactions.InteractionTextInput) assert short_text_input.value == "Wumpus" assert short_text_input.type == message_models.ComponentType.TEXT_INPUT assert short_text_input.custom_id == "name" diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py index 4626778449..7df3d9cf8e 100644 --- a/tests/hikari/interactions/test_modal_interactions.py +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -57,7 +57,7 @@ def mock_modal_interaction(self, mock_app): guild_locale="en-US", components=special_endpoints.ActionRowBuilder( components=[ - modal_interactions.PartialTextInput( + modal_interactions.InteractionTextInput( type=messages.ComponentType.TEXT_INPUT, custom_id="le id", value="le value" ) ], From ea870b98a126bf6de08bcc77363a8870ca17716b Mon Sep 17 00:00:00 2001 From: sadru Date: Sat, 26 Mar 2022 00:31:28 +0100 Subject: [PATCH 17/40] increase test coverage --- hikari/impl/rest.py | 5 +++ hikari/impl/special_endpoints.py | 5 +-- hikari/interactions/base_interactions.py | 2 +- tests/hikari/impl/test_entity_factory.py | 22 +++++++++++++ tests/hikari/impl/test_special_endpoints.py | 31 +++++++++++++++++++ .../interactions/test_base_interactions.py | 11 +++++++ .../interactions/test_modal_interactions.py | 12 +++++++ 7 files changed, 85 insertions(+), 3 deletions(-) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 72dd51e924..3cdfc5fd46 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -3401,6 +3401,11 @@ def interaction_modal_builder( *, components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, ) -> special_endpoints.InteractionModalBuilder: + if components is undefined.UNDEFINED: + components = [] + else: + components = list(components) + return special_endpoints_impl.InteractionModalBuilder( title=title, custom_id=custom_id, diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 503880e9ba..046826cfbe 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -1003,7 +1003,7 @@ class InteractionModalBuilder(special_endpoints.InteractionModalBuilder): _title: str = attr.field() _custom_id: str = attr.field() - _components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = attr.field() + _components: typing.List[special_endpoints.ComponentBuilder] = attr.field(factory=list) @property def type(self) -> typing.Literal[base_interactions.ResponseType.MODAL]: @@ -1018,7 +1018,7 @@ def custom_id(self) -> str: return self._custom_id @property - def components(self) -> undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]]: + def components(self) -> typing.Sequence[special_endpoints.ComponentBuilder]: return self._components def set_title(self: _InteractionModalBuilderT, title: str, /) -> _InteractionModalBuilderT: @@ -1032,6 +1032,7 @@ def set_custom_id(self: _InteractionModalBuilderT, custom_id: str, /) -> _Intera def add_component( self: _InteractionModalBuilderT, component: special_endpoints.ComponentBuilder, / ) -> _InteractionModalBuilderT: + self._components.append(component) return self def build(self, entity_factory: entity_factory_.EntityFactory, /) -> data_binding.JSONObject: diff --git a/hikari/interactions/base_interactions.py b/hikari/interactions/base_interactions.py index 873cf37d27..9f978336b6 100644 --- a/hikari/interactions/base_interactions.py +++ b/hikari/interactions/base_interactions.py @@ -578,7 +578,7 @@ async def delete_initial_response(self) -> None: class ModalResponseMixin(PartialInteraction): - """Mixin' class for all interaction types which can be responded to with a message.""" + """Mixin' class for all interaction types which can be responded to with a modal.""" __slots__: typing.Sequence[str] = () diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index c3edefd739..e1a8df24fc 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -3582,6 +3582,28 @@ def test_deserialize_modal_interaction( assert short_text_input.type == message_models.ComponentType.TEXT_INPUT assert short_text_input.custom_id == "name" + def test_deserialize_modal_interaction_with_user( + self, + entity_factory_impl, + modal_interaction_payload, + user_payload, + ): + modal_interaction_payload["member"] = None + modal_interaction_payload["user"] = user_payload + + interaction = entity_factory_impl.deserialize_modal_interaction(modal_interaction_payload) + assert interaction.user.id == 115590097100865541 + + def test_deserialize_modal_interaction_with_unrecognized_component( + self, + entity_factory_impl, + modal_interaction_payload, + ): + modal_interaction_payload["data"]["components"] = [{"type": 0}] + + interaction = entity_factory_impl.deserialize_modal_interaction(modal_interaction_payload) + assert len(interaction.components) == 0 + ################## # STICKER MODELS # ################## diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py index 23dec9e114..60f6421b65 100644 --- a/tests/hikari/impl/test_special_endpoints.py +++ b/tests/hikari/impl/test_special_endpoints.py @@ -440,6 +440,37 @@ def test_build_handles_attachments(self): builder.build(mock_entity_factory) +class TestInteractionModalBuilder: + def test_type_property(self): + builder = special_endpoints.InteractionModalBuilder("title", "custom_id") + assert builder.type == 9 + + def test_title_property(self): + builder = special_endpoints.InteractionModalBuilder("title", "custom_id").set_title("title2") + assert builder.title == "title2" + + def test_custom_id_property(self): + builder = special_endpoints.InteractionModalBuilder("title", "custom_id").set_custom_id("better_custom_id") + assert builder.custom_id == "better_custom_id" + + def test_components_property(self): + component = mock.Mock() + builder = special_endpoints.InteractionModalBuilder("title", "custom_id").add_component(component) + assert builder.components == [component] + + def test_build(self): + component = mock.Mock() + builder = special_endpoints.InteractionModalBuilder("title", "custom_id").add_component(component) + assert builder.build(mock.Mock()) == { + "type": 9, + "data": { + "title": "title", + "custom_id": "custom_id", + "components": [component.build.return_value], + }, + } + + class TestSlashCommandBuilder: def test_description_property(self): builder = special_endpoints.SlashCommandBuilder("ok", "NO") diff --git a/tests/hikari/interactions/test_base_interactions.py b/tests/hikari/interactions/test_base_interactions.py index 6c95f5d987..d2bc034283 100644 --- a/tests/hikari/interactions/test_base_interactions.py +++ b/tests/hikari/interactions/test_base_interactions.py @@ -224,3 +224,14 @@ async def test_create_modal_response(self, mock_modal_response_mixin, mock_app): custom_id="custom_id", components=[], ) + + def test_build_response(self, mock_modal_response_mixin, mock_app): + mock_app.rest.interaction_modal_builder = mock.Mock() + builder = mock_modal_response_mixin.build_modal_response("title", "custom_id", components=[]) + + assert builder is mock_app.rest.interaction_modal_builder.return_value + mock_app.rest.interaction_modal_builder.assert_called_once_with( + title="title", + custom_id="custom_id", + components=[], + ) diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py index 7df3d9cf8e..465a5aef34 100644 --- a/tests/hikari/interactions/test_modal_interactions.py +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -147,6 +147,13 @@ async def test_fetch_parent_message(self): stub_interaction.fetch_message.assert_awaited_once_with(3421) + @pytest.mark.asyncio() + async def test_fetch_parent_message_when_none(self): + stub_interaction = hikari_test_helpers.mock_class_namespace(modal_interactions.ModalInteraction, init_=False)() + stub_interaction.message = None + + assert await stub_interaction.fetch_parent_message() is None + def test_get_parent_message(self, mock_modal_interaction, mock_app): mock_modal_interaction.message = mock.Mock(id=321655) @@ -160,3 +167,8 @@ def test_get_parent_message_when_cacheless(self, mock_modal_interaction, mock_ap assert mock_modal_interaction.get_parent_message() is None mock_app.cache.get_message.assert_not_called() + + def test_get_parent_message_when_none(self, mock_modal_interaction, mock_app): + mock_modal_interaction.message = None + + assert mock_modal_interaction.get_parent_message() is None From a7e9b04eae862741bc89efaef6065470b8d91259 Mon Sep 17 00:00:00 2001 From: sadru Date: Sat, 2 Apr 2022 00:06:29 +0200 Subject: [PATCH 18/40] Get rid of ModalInteractionActionRow --- hikari/interactions/modal_interactions.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index 90779203e8..31c9a5e3fb 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -70,21 +70,6 @@ class InteractionTextInput(messages.PartialComponent): """Value provided for this text input.""" -class ModalInteractionActionRow(typing.Protocol): - """An action row with only partial text inputs. - - Meant purely for use with ModalInteraction. - """ - - __slots__ = () - - type: typing.Literal[messages.ComponentType.ACTION_ROW] - """The type of component this is.""" - - components: typing.List[InteractionTextInput] - """Sequence of the components contained within this row.""" - - @attr_extensions.with_copy @attr.define(hash=True, kw_only=True, weakref_slot=False) class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypesT]): @@ -134,7 +119,7 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes locale: str = attr.field(eq=False, hash=False, repr=True) """The selected language of the user who triggered this modal interaction.""" - components: typing.Sequence[ModalInteractionActionRow] = attr.field(eq=False, hash=False, repr=True) + components: typing.Sequence[messages.ActionRowComponent] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" async def fetch_channel(self) -> channels.TextableChannel: From 37d4ac5cfca07e70fb0cadb43989bbe5afe618c9 Mon Sep 17 00:00:00 2001 From: sadru Date: Tue, 5 Apr 2022 20:05:07 +0200 Subject: [PATCH 19/40] Remove redundant property docstrings --- hikari/api/special_endpoints.py | 345 +++----------------------------- 1 file changed, 33 insertions(+), 312 deletions(-) diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index e8a1030000..80af05a5a9 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -187,13 +187,7 @@ class GuildBuilder(abc.ABC): @property @abc.abstractmethod def name(self) -> str: - """Name of the guild to create. - - Returns - ------- - builtins.str - The guild name. - """ + """Name of the guild to create.""" @property @abc.abstractmethod @@ -201,11 +195,6 @@ def default_message_notifications(self) -> undefined.UndefinedOr[guilds.GuildMes """Default message notification level that can be overwritten. If not overridden, this will use the Discord default level. - - Returns - ------- - hikari.undefined.UndefinedOr[hikari.guilds.GuildMessageNotificationsLevel] - The default message notification level, if overwritten. """ # noqa: D401 - Imperative mood @default_message_notifications.setter @@ -220,11 +209,6 @@ def explicit_content_filter_level(self) -> undefined.UndefinedOr[guilds.GuildExp """Explicit content filter level that can be overwritten. If not overridden, this will use the Discord default level. - - Returns - ------- - hikari.undefined.UndefinedOr[hikari.guilds.GuildExplicitContentFilterLevel] - The explicit content filter level, if overwritten. """ @explicit_content_filter_level.setter @@ -239,11 +223,6 @@ def icon(self) -> undefined.UndefinedOr[files.Resourceish]: """Guild icon to use that can be overwritten. If not overridden, the guild will not have an icon. - - Returns - ------- - hikari.undefined.UndefinedOr[hikari.files.Resourceish] - The guild icon to use, if overwritten. """ @icon.setter @@ -256,11 +235,6 @@ def verification_level(self) -> undefined.UndefinedOr[typing.Union[guilds.GuildV """Verification level required to join the guild that can be overwritten. If not overridden, the guild will use the default verification level for - - Returns - ------- - hikari.undefined.UndefinedOr[typing.Union[hikari.guilds.GuildVerificationLevel, builtins.int]] - The verification level required to join the guild, if overwritten. """ @verification_level.setter @@ -560,13 +534,7 @@ class InteractionResponseBuilder(abc.ABC): @property @abc.abstractmethod def type(self) -> typing.Union[int, base_interactions.ResponseType]: - """Return the type of this response. - - Returns - ------- - typing.Union[builtins.int, hikari.interactions.base_interactions.ResponseType] - The type of response this is. - """ + """Return the type of this response.""" @abc.abstractmethod def build( @@ -595,13 +563,7 @@ class InteractionDeferredBuilder(InteractionResponseBuilder, abc.ABC): @property @abc.abstractmethod def type(self) -> base_interactions.DeferredResponseTypesT: - """Return the type of this response. - - Returns - ------- - hikari.interactions.base_interactions.DeferredResponseTypesT - The type of response this is. - """ + """Return the type of this response.""" @property @abc.abstractmethod @@ -611,12 +573,6 @@ def flags(self) -> typing.Union[undefined.UndefinedType, int, messages.MessageFl !!! note As of writing the only message flag which can be set here is `hikari.messages.MessageFlag.EPHEMERAL`. - - Returns - ------- - typing.Union[hikari.undefined.UndefinedType, builtins.int, hikari.messages.MessageFlag] - The message flags this response should have if set else - `hikari.undefined.UNDEFINED`. """ @abc.abstractmethod @@ -674,13 +630,7 @@ class InteractionMessageBuilder(InteractionResponseBuilder, abc.ABC): @property @abc.abstractmethod def type(self) -> base_interactions.MessageResponseTypesT: - """Return the type of this response. - - Returns - ------- - hikari.interactions.base_interactions.MessageResponseTypesT - The type of response this is. - """ + """Return the type of this response.""" # Extendable fields @@ -704,13 +654,7 @@ def embeds(self) -> undefined.UndefinedOr[typing.Sequence[embeds_.Embed]]: @property @abc.abstractmethod def content(self) -> undefined.UndefinedOr[str]: - """Response's message content. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.str] - The response's message content, if set. - """ + """Response's message content.""" @property @abc.abstractmethod @@ -720,37 +664,17 @@ def flags(self) -> typing.Union[undefined.UndefinedType, int, messages.MessageFl !!! note As of writing the only message flag which can be set here is `hikari.messages.MessageFlag.EPHEMERAL`. - - Returns - ------- - typing.Union[hikari.undefined.UndefinedType, builtins.int, hikari.messages.MessageFlag] - The message flags this response should have if set else - `hikari.undefined.UNDEFINED`. """ @property @abc.abstractmethod def is_tts(self) -> undefined.UndefinedOr[bool]: - """Whether this response's content should be treated as text-to-speech. - - Returns - ------- - builtins.bool - Whether this response's content should be treated as text-to-speech. - If left as `hikari.undefined.UNDEFINED` then this will be disabled. - """ + """Whether this response's content should be treated as text-to-speech.""" @property @abc.abstractmethod def mentions_everyone(self) -> undefined.UndefinedOr[bool]: - """Whether @everyone and @here mentions should be enabled for this response. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.bool] - Whether @everyone mentions should be enabled for this response. - If left as `hikari.undefined.UNDEFINED` then they will be disabled. - """ + """Whether @everyone and @here mentions should be enabled for this response.""" @property @abc.abstractmethod @@ -949,46 +873,22 @@ class InteractionModalBuilder(InteractionResponseBuilder, abc.ABC): @property @abc.abstractmethod def type(self) -> typing.Literal[base_interactions.ResponseType.MODAL]: - """Return the type of this response. - - Returns - ------- - hikari.interactions.base_interactions.MessageResponseTypesT - The type of response this is. - """ + """Return the type of this response.""" @property @abc.abstractmethod def title(self) -> str: - """Return the title that will show up in the modal. - - Returns - ------- - builtins.str - The title that will show up in the modal. - """ + """Return the title that will show up in the modal.""" @property @abc.abstractmethod def custom_id(self) -> str: - """Return the developer set custom ID used for identifying interactions with this modal. - - Returns - ------- - builtins.str - Developer set custom ID used for identifying interactions with this modal. - """ + """Return the developer set custom ID used for identifying interactions with this modal.""" @property @abc.abstractmethod def components(self) -> undefined.UndefinedOr[typing.Sequence[ComponentBuilder]]: - """Return the sequence of component builders to send in this modal. - - Returns - ------- - hikari.undefined.UndefinedOr[typing.Sequence[hikari.api.special_endpoints.ComponentBuilder]] - A sequence of component builders to send in this modal. - """ + """Return the sequence of component builders to send in this modal.""" @abc.abstractmethod def set_title(self: _T, title: str, /) -> _T: @@ -1059,24 +959,12 @@ def name(self) -> str: @property @abc.abstractmethod def type(self) -> commands.CommandType: - """Return the type of this command. - - Returns - ------- - hikari.commands.CommandType - The type of this command. - """ + """Return the type of this command.""" @property @abc.abstractmethod def id(self) -> undefined.UndefinedOr[snowflakes.Snowflake]: - """ID of this command. - - Returns - ------- - hikari.undefined.UndefinedOr[hikari.snowflakes.Snowflake] - The ID of this command if set. - """ + """ID of this command.""" @property @abc.abstractmethod @@ -1084,11 +972,6 @@ def default_permission(self) -> undefined.UndefinedOr[bool]: """Whether the command should be enabled by default (without any permissions). Defaults to `builtins.bool`. - - Returns - ------- - undefined.UndefinedOr[builtins.bool] - Whether the command should be enabled by default (without any permissions). """ @abc.abstractmethod @@ -1180,23 +1063,12 @@ def description(self) -> str: !!! warning This should be inclusively between 1-100 characters in length. - - Returns - ------- - builtins.str - The description to set for this command. """ @property @abc.abstractmethod def options(self) -> typing.Sequence[commands.CommandOption]: - """Sequence of up to 25 of the options set for this command. - - Returns - ------- - typing.Sequence[hikari.commands.CommandOption] - A sequence of up to 25 of the options set for this command. - """ + """Sequence of up to 25 of the options set for this command.""" @abc.abstractmethod def add_option(self: _T, option: commands.CommandOption) -> _T: @@ -1316,25 +1188,12 @@ class ButtonBuilder(ComponentBuilder, abc.ABC, typing.Generic[_ContainerT]): @property @abc.abstractmethod def style(self) -> typing.Union[messages.ButtonStyle, int]: - """Button's style. - - Returns - ------- - typing.Union[builtins.int, hikari.messages.ButtonStyle] - The button's style. - """ + """Button's style.""" @property @abc.abstractmethod def emoji(self) -> typing.Union[snowflakes.Snowflakeish, emojis.Emoji, str, undefined.UndefinedType]: - """Emoji which should appear on this button. - - Returns - ------- - typing.Union[hikari.snowflakes.Snowflakeish, hikari.emojis.Emoji, builtins.str, hikari.undefined.UndefinedType] - Object or ID or raw string of the emoji which should be displayed - on this button if set. - """ + """Emoji which should appear on this button.""" @property @abc.abstractmethod @@ -1344,11 +1203,6 @@ def label(self) -> undefined.UndefinedOr[str]: !!! note The text label to that should appear on this button. This may be up to 80 characters long. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.str] - Text label which should appear on this button. """ @property @@ -1358,11 +1212,6 @@ def is_disabled(self) -> bool: !!! note Defaults to `builtins.False`. - - Returns - ------- - builtins.bool - Whether the button should be marked as disabled. """ @abc.abstractmethod @@ -1436,13 +1285,7 @@ class LinkButtonBuilder(ButtonBuilder[_ContainerT], abc.ABC): @property @abc.abstractmethod def url(self) -> str: - """Url this button should link to when pressed. - - Returns - ------- - builtins.str - Url this button should link to when pressed. - """ + """Url this button should link to when pressed.""" class InteractiveButtonBuilder(ButtonBuilder[_ContainerT], abc.ABC): @@ -1453,13 +1296,7 @@ class InteractiveButtonBuilder(ButtonBuilder[_ContainerT], abc.ABC): @property @abc.abstractmethod def custom_id(self) -> str: - """Developer set custom ID used for identifying interactions with this button. - - Returns - ------- - builtins.str - Developer set custom ID used for identifying interactions with this button. - """ + """Developer set custom ID used for identifying interactions with this button.""" class SelectOptionBuilder(ComponentBuilder, abc.ABC, typing.Generic[_SelectMenuBuilderT]): @@ -1470,47 +1307,22 @@ class SelectOptionBuilder(ComponentBuilder, abc.ABC, typing.Generic[_SelectMenuB @property @abc.abstractmethod def label(self) -> str: - """User-facing name of the option, max 100 characters. - - Returns - ------- - builtins.str - User-facing name of the option. - """ + """User-facing name of the option, max 100 characters.""" @property @abc.abstractmethod def value(self) -> str: - """Developer-defined value of the option, max 100 characters. - - Returns - ------- - builtins.str - Developer-defined value of the option. - """ + """Developer-defined value of the option, max 100 characters.""" @property @abc.abstractmethod def description(self) -> undefined.UndefinedOr[str]: - """Return the description of the option, max 100 characters. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.str] - Description of the option, if set. - """ + """Return the description of the option, max 100 characters.""" @property @abc.abstractmethod def emoji(self) -> typing.Union[snowflakes.Snowflakeish, emojis.Emoji, str, undefined.UndefinedType]: - """Emoji which should appear on this option. - - Returns - ------- - typing.Union[hikari.snowflakes.Snowflakeish, hikari.emojis.Emoji, builtins.str, hikari.undefined.UndefinedType] - Object or ID or raw string of the emoji which should be displayed - on this option if set. - """ + """Emoji which should appear on this option.""" @property @abc.abstractmethod @@ -1518,11 +1330,6 @@ def is_default(self) -> bool: """Whether this option should be marked as selected by default. Defaults to `builtins.False`. - - Returns - ------- - builtins.bool - Whether this option should be marked as selected by default. """ @abc.abstractmethod @@ -1595,13 +1402,7 @@ class SelectMenuBuilder(ComponentBuilder, abc.ABC, typing.Generic[_ContainerT]): @property @abc.abstractmethod def custom_id(self) -> str: - """Developer set custom ID used for identifying interactions with this menu. - - Returns - ------- - builtins.str - Developer set custom ID used for identifying interactions with this menu. - """ + """Developer set custom ID used for identifying interactions with this menu.""" @property @abc.abstractmethod @@ -1610,34 +1411,17 @@ def is_disabled(self) -> bool: !!! note Defaults to `builtins.False`. - - Returns - ------- - builtins.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. - - Returns - ------- - typing.Sequence[SelectOptionBuilder[Self]] - Sequence of the options set for this select menu. - """ + """Sequence of the options set for this select menu.""" @property @abc.abstractmethod def placeholder(self) -> undefined.UndefinedOr[str]: - """Return the placeholder text to display when no options are selected. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.str] - Placeholder text to display when no options are selected, if defined. - """ + """Return the placeholder text to display when no options are selected.""" @property @abc.abstractmethod @@ -1647,11 +1431,6 @@ def min_values(self) -> int: Defaults to 1. Must be less than or equal to `SelectMenuBuilder.max_values` and greater than or equal to 0. - - Returns - ------- - builtins.str - Minimum number of options which must be chosen. """ @property @@ -1662,11 +1441,6 @@ def max_values(self) -> int: Defaults to 1. Must be greater than or equal to `SelectMenuBuilder.min_values` and less than or equal to 25. - - Returns - ------- - builtins.str - Maximum number of options which can be chosen. """ @abc.abstractmethod @@ -1782,89 +1556,42 @@ def custom_id(self) -> str: !!! note This custom_id is never used in component interaction events. It is meant to be used purely for resolving components modal interactions. - - Returns - ------- - builtins.str - Developer set custom ID used for identifying this text input. """ @property @abc.abstractmethod def label(self) -> str: - """Label above this text input. - - Returns - ------- - builtins.str - Label above this text input. - """ + """Label above this text input.""" @property @abc.abstractmethod def style(self) -> messages.TextInputStyle: - """Style to use for the text input. - - Returns - ------- - builtins.str - Style to use for the text input. - """ + """Style to use for the text input.""" @property @abc.abstractmethod def placeholder(self) -> undefined.UndefinedOr[str]: - """Return the placeholder text for when the text input is empty. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.str] - Placeholder text for when the text input is empty. - """ + """Return the placeholder text for when the text input is empty.""" @property @abc.abstractmethod def value(self) -> undefined.UndefinedOr[str]: - """Pre-filled text that will be sent if the user does not write anything. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.str] - Pre-filled text that will be sent if the user does not write anything. - """ + """Pre-filled text that will be sent if the user does not write anything.""" @property @abc.abstractmethod def required(self) -> undefined.UndefinedOr[bool]: - """Whether this text input is required to be filled-in. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.bool] - Whether this text input is required to be filled-in. - """ + """Whether this text input is required to be filled-in.""" @property @abc.abstractmethod def min_length(self) -> undefined.UndefinedOr[int]: - """Return the minimum length the text should have. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.int] - The minimum length the text should have. - """ + """Return the minimum length the text should have.""" @property @abc.abstractmethod def max_length(self) -> undefined.UndefinedOr[int]: - """Return the maxmimum length the text should have. - - Returns - ------- - hikari.undefined.UndefinedOr[builtins.int] - The maxmimum length the text should have. - """ + """Return the maxmimum length the text should have.""" @abc.abstractmethod def set_style(self: _T, style: typing.Union[messages.TextInputStyle, int], /) -> _T: @@ -2005,13 +1732,7 @@ class ActionRowBuilder(ComponentBuilder, abc.ABC): @property @abc.abstractmethod def components(self) -> typing.Sequence[ComponentBuilder]: - """Sequence of the component builders registered within this action row. - - Returns - ------- - typing.Sequence[ComponentBuilder] - Sequence of the component builders registered within this action row. - """ + """Sequence of the component builders registered within this action row.""" @abc.abstractmethod def add_component( From 934b0b1012db0e6f00ddd9759fbd995cde027442 Mon Sep 17 00:00:00 2001 From: sadru Date: Tue, 5 Apr 2022 20:10:34 +0200 Subject: [PATCH 20/40] Remove parent_message in interactions --- hikari/interactions/component_interactions.py | 3 ++ hikari/interactions/modal_interactions.py | 51 ------------------- .../test_component_interactions.py | 25 --------- .../interactions/test_modal_interactions.py | 37 -------------- 4 files changed, 3 insertions(+), 113 deletions(-) diff --git a/hikari/interactions/component_interactions.py b/hikari/interactions/component_interactions.py index 03dec8f598..df49f59e76 100644 --- a/hikari/interactions/component_interactions.py +++ b/hikari/interactions/component_interactions.py @@ -32,6 +32,7 @@ from hikari import channels from hikari import traits from hikari.interactions import base_interactions +from hikari.internal import deprecation if typing.TYPE_CHECKING: from hikari import guilds @@ -319,6 +320,7 @@ def get_guild(self) -> typing.Optional[guilds.GatewayGuild]: return None + @deprecation.deprecated("2.0.0.dev110", "message") async def fetch_parent_message(self) -> messages.Message: """Fetch the message which this interaction was triggered on. @@ -351,6 +353,7 @@ async def fetch_parent_message(self) -> messages.Message: """ return await self.fetch_message(self.message.id) + @deprecation.deprecated("2.0.0.dev110", "message") def get_parent_message(self) -> typing.Optional[messages.PartialMessage]: """Get the message which this interaction was triggered on from the cache. diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index 31c9a5e3fb..aa41329e17 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -229,57 +229,6 @@ def get_guild(self) -> typing.Optional[guilds.GatewayGuild]: return None - async def fetch_parent_message(self) -> typing.Optional[messages.Message]: - """Fetch the message which this interaction was triggered on. - - Returns - ------- - hikari.messages.Message - The requested message. - - Raises - ------ - builtins.ValueError - If `token` is not available. - hikari.errors.UnauthorizedError - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.NotFoundError - If the webhook is not found or the webhook's message wasn't found. - hikari.errors.RateLimitTooLongError - Raised in the event that a rate limit occurs that is - longer than `max_rate_limit` when making a request. - hikari.errors.RateLimitedError - Usually, Hikari will handle and retry on hitting - rate-limits automatically. This includes most bucket-specific - rate-limits and global rate-limits. In some rare edge cases, - however, Discord implements other undocumented rules for - rate-limiting, such as limits per attribute. These cannot be - detected or handled normally by Hikari due to their undocumented - nature, and will trigger this exception if they occur. - hikari.errors.InternalServerError - If an internal error occurs on Discord while handling the request. - """ - if self.message is None: - return None - - return await self.fetch_message(self.message.id) - - def get_parent_message(self) -> typing.Optional[messages.PartialMessage]: - """Get the message which this interaction was triggered on from the cache. - - Returns - ------- - typing.Optional[hikari.messages.Message] - The object of the message if found or `builtins.None`. - """ - if self.message is None: - return None - - if isinstance(self.app, traits.CacheAware): - return self.app.cache.get_message(self.message.id) - - return None - def build_response(self) -> special_endpoints.InteractionMessageBuilder: """Get a message response builder for use in the REST server flow. diff --git a/tests/hikari/interactions/test_component_interactions.py b/tests/hikari/interactions/test_component_interactions.py index d96358c62d..ff9765f347 100644 --- a/tests/hikari/interactions/test_component_interactions.py +++ b/tests/hikari/interactions/test_component_interactions.py @@ -136,28 +136,3 @@ def test_get_guild_when_cacheless(self, mock_component_interaction, mock_app): assert mock_component_interaction.get_guild() is None mock_app.cache.get_guild.assert_not_called() - - @pytest.mark.asyncio() - async def test_fetch_parent_message(self): - stub_interaction = hikari_test_helpers.mock_class_namespace( - component_interactions.ComponentInteraction, fetch_message=mock.AsyncMock(), init_=False - )() - stub_interaction.message = mock.Mock(id=3421) - - assert await stub_interaction.fetch_parent_message() is stub_interaction.fetch_message.return_value - - stub_interaction.fetch_message.assert_awaited_once_with(3421) - - def test_get_parent_message(self, mock_component_interaction, mock_app): - mock_component_interaction.message = mock.Mock(id=321655) - - assert mock_component_interaction.get_parent_message() is mock_app.cache.get_message.return_value - - mock_app.cache.get_message.assert_called_once_with(321655) - - def test_get_parent_message_when_cacheless(self, mock_component_interaction, mock_app): - mock_component_interaction.app = mock.Mock(traits.RESTAware) - - assert mock_component_interaction.get_parent_message() is None - - mock_app.cache.get_message.assert_not_called() diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py index 465a5aef34..6f4612982f 100644 --- a/tests/hikari/interactions/test_modal_interactions.py +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -135,40 +135,3 @@ def test_get_guild_when_cacheless(self, mock_modal_interaction, mock_app): assert mock_modal_interaction.get_guild() is None mock_app.cache.get_guild.assert_not_called() - - @pytest.mark.asyncio() - async def test_fetch_parent_message(self): - stub_interaction = hikari_test_helpers.mock_class_namespace( - modal_interactions.ModalInteraction, fetch_message=mock.AsyncMock(), init_=False - )() - stub_interaction.message = mock.Mock(id=3421) - - assert await stub_interaction.fetch_parent_message() is stub_interaction.fetch_message.return_value - - stub_interaction.fetch_message.assert_awaited_once_with(3421) - - @pytest.mark.asyncio() - async def test_fetch_parent_message_when_none(self): - stub_interaction = hikari_test_helpers.mock_class_namespace(modal_interactions.ModalInteraction, init_=False)() - stub_interaction.message = None - - assert await stub_interaction.fetch_parent_message() is None - - def test_get_parent_message(self, mock_modal_interaction, mock_app): - mock_modal_interaction.message = mock.Mock(id=321655) - - assert mock_modal_interaction.get_parent_message() is mock_app.cache.get_message.return_value - - mock_app.cache.get_message.assert_called_once_with(321655) - - def test_get_parent_message_when_cacheless(self, mock_modal_interaction, mock_app): - mock_modal_interaction.app = mock.Mock(traits.RESTAware) - - assert mock_modal_interaction.get_parent_message() is None - - mock_app.cache.get_message.assert_not_called() - - def test_get_parent_message_when_none(self, mock_modal_interaction, mock_app): - mock_modal_interaction.message = None - - assert mock_modal_interaction.get_parent_message() is None From 983dda82bb8eaecbdd315fda72d411c6e1050327 Mon Sep 17 00:00:00 2001 From: sadru Date: Tue, 5 Apr 2022 20:12:58 +0200 Subject: [PATCH 21/40] Make the towncrier changelog entry somewhat sensible --- changes/1002.feature.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/1002.feature.md b/changes/1002.feature.md index 52e43755f0..24d0f6c27a 100644 --- a/changes/1002.feature.md +++ b/changes/1002.feature.md @@ -1 +1 @@ -Implement modals +Implement modal interactions. From b5339a670066b66d684b9a607beabfd9b2622d48 Mon Sep 17 00:00:00 2001 From: sadru Date: Tue, 5 Apr 2022 20:19:51 +0200 Subject: [PATCH 22/40] Fix inconsistent wording in property docstrings --- hikari/api/special_endpoints.py | 30 +++++++++---------- hikari/interactions/component_interactions.py | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 80af05a5a9..5edffb9764 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -534,7 +534,7 @@ class InteractionResponseBuilder(abc.ABC): @property @abc.abstractmethod def type(self) -> typing.Union[int, base_interactions.ResponseType]: - """Return the type of this response.""" + """Type of this response.""" @abc.abstractmethod def build( @@ -563,7 +563,7 @@ class InteractionDeferredBuilder(InteractionResponseBuilder, abc.ABC): @property @abc.abstractmethod def type(self) -> base_interactions.DeferredResponseTypesT: - """Return the type of this response.""" + """Type of this response.""" @property @abc.abstractmethod @@ -602,7 +602,7 @@ class InteractionAutocompleteBuilder(InteractionResponseBuilder, abc.ABC): @property @abc.abstractmethod def choices(self) -> typing.Sequence[commands.CommandChoice]: - """Return autocomplete choices.""" + """Autocomplete choices.""" @abc.abstractmethod def set_choices(self: _T, choices: typing.Sequence[commands.CommandChoice], /) -> _T: @@ -630,7 +630,7 @@ class InteractionMessageBuilder(InteractionResponseBuilder, abc.ABC): @property @abc.abstractmethod def type(self) -> base_interactions.MessageResponseTypesT: - """Return the type of this response.""" + """Type of this response.""" # Extendable fields @@ -873,22 +873,22 @@ class InteractionModalBuilder(InteractionResponseBuilder, abc.ABC): @property @abc.abstractmethod def type(self) -> typing.Literal[base_interactions.ResponseType.MODAL]: - """Return the type of this response.""" + """Type of this response.""" @property @abc.abstractmethod def title(self) -> str: - """Return the title that will show up in the modal.""" + """Title that will show up in the modal.""" @property @abc.abstractmethod def custom_id(self) -> str: - """Return the developer set custom ID used for identifying interactions with this modal.""" + """Developer set custom ID used for identifying interactions with this modal.""" @property @abc.abstractmethod def components(self) -> undefined.UndefinedOr[typing.Sequence[ComponentBuilder]]: - """Return the sequence of component builders to send in this modal.""" + """Sequence of component builders to send in this modal.""" @abc.abstractmethod def set_title(self: _T, title: str, /) -> _T: @@ -959,7 +959,7 @@ def name(self) -> str: @property @abc.abstractmethod def type(self) -> commands.CommandType: - """Return the type of this command.""" + """Type of this command.""" @property @abc.abstractmethod @@ -1059,7 +1059,7 @@ class SlashCommandBuilder(CommandBuilder): @property @abc.abstractmethod def description(self) -> str: - """Return the description to set for this command. + """Description to set for this command. !!! warning This should be inclusively between 1-100 characters in length. @@ -1317,7 +1317,7 @@ def value(self) -> str: @property @abc.abstractmethod def description(self) -> undefined.UndefinedOr[str]: - """Return the description of the option, max 100 characters.""" + """Description of the option, max 100 characters.""" @property @abc.abstractmethod @@ -1421,7 +1421,7 @@ def options(self: _SelectMenuBuilderT) -> typing.Sequence[SelectOptionBuilder[_S @property @abc.abstractmethod def placeholder(self) -> undefined.UndefinedOr[str]: - """Return the placeholder text to display when no options are selected.""" + """Placeholder text to display when no options are selected.""" @property @abc.abstractmethod @@ -1571,7 +1571,7 @@ def style(self) -> messages.TextInputStyle: @property @abc.abstractmethod def placeholder(self) -> undefined.UndefinedOr[str]: - """Return the placeholder text for when the text input is empty.""" + """Placeholder text for when the text input is empty.""" @property @abc.abstractmethod @@ -1586,12 +1586,12 @@ def required(self) -> undefined.UndefinedOr[bool]: @property @abc.abstractmethod def min_length(self) -> undefined.UndefinedOr[int]: - """Return the minimum length the text should have.""" + """Minimum length the text should have.""" @property @abc.abstractmethod def max_length(self) -> undefined.UndefinedOr[int]: - """Return the maxmimum length the text should have.""" + """Maxmimum length the text should have.""" @abc.abstractmethod def set_style(self: _T, style: typing.Union[messages.TextInputStyle, int], /) -> _T: diff --git a/hikari/interactions/component_interactions.py b/hikari/interactions/component_interactions.py index df49f59e76..1cb127da24 100644 --- a/hikari/interactions/component_interactions.py +++ b/hikari/interactions/component_interactions.py @@ -320,7 +320,7 @@ def get_guild(self) -> typing.Optional[guilds.GatewayGuild]: return None - @deprecation.deprecated("2.0.0.dev110", "message") + @deprecation.deprecated("2.0.0.dev110", "fetch_message") async def fetch_parent_message(self) -> messages.Message: """Fetch the message which this interaction was triggered on. From 2fe70060c7a0d0432430571c8acbc7b913489ea1 Mon Sep 17 00:00:00 2001 From: thesadru Date: Wed, 6 Apr 2022 08:28:58 +0200 Subject: [PATCH 23/40] Do recommended changes --- hikari/api/special_endpoints.py | 8 +++--- hikari/impl/entity_factory.py | 4 +-- hikari/interactions/modal_interactions.py | 2 +- tests/hikari/impl/test_rest.py | 8 ++++++ .../test_component_interactions.py | 25 +++++++++++++++++++ .../interactions/test_modal_interactions.py | 1 - 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 5edffb9764..058c4f714b 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -1063,7 +1063,7 @@ def description(self) -> str: !!! warning This should be inclusively between 1-100 characters in length. - """ + """ # noqa: D401 - Imperative mood @property @abc.abstractmethod @@ -1317,7 +1317,7 @@ def value(self) -> str: @property @abc.abstractmethod def description(self) -> undefined.UndefinedOr[str]: - """Description of the option, max 100 characters.""" + """Description of the option, max 100 characters.""" # noqa: D401 - Imperative mood @property @abc.abstractmethod @@ -1421,7 +1421,7 @@ def options(self: _SelectMenuBuilderT) -> typing.Sequence[SelectOptionBuilder[_S @property @abc.abstractmethod def placeholder(self) -> undefined.UndefinedOr[str]: - """Placeholder text to display when no options are selected.""" + """Placeholder text to display when no options are selected.""" # noqa: D401 - Imperative mood @property @abc.abstractmethod @@ -1571,7 +1571,7 @@ def style(self) -> messages.TextInputStyle: @property @abc.abstractmethod def placeholder(self) -> undefined.UndefinedOr[str]: - """Placeholder text for when the text input is empty.""" + """Placeholder text for when the text input is empty.""" # noqa: D401 - Imperative mood @property @abc.abstractmethod diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 893664e208..5d934649ff 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -2134,8 +2134,8 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod id=snowflakes.Snowflake(payload["id"]), type=base_interactions.InteractionType(payload["type"]), guild_id=guild_id, - guild_locale=payload.get("guild_locale", "en-US"), - locale=payload["locale"], + guild_locale=locales.Locale(payload["guild_locale"]) if "guild_locale" in payload else None, + locale=locales.Locale(payload["locale"]), channel_id=snowflakes.Snowflake(payload["channel_id"]), member=member, user=user, diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index aa41329e17..a24b8190c2 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # cython: language_level=3 # Copyright (c) 2020 Nekokatt -# Copyright (c) 2021 davfsa +# Copyright (c) 2021-present davfsa # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 588548f76c..bdb338b7dd 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -1388,6 +1388,14 @@ def test_interaction_modal_builder(self, rest_client): assert result.type == 9 assert isinstance(result, special_endpoints.InteractionModalBuilder) + def test_interaction_modal_builder_with_components(self, rest_client): + component = mock.Mock() + result = rest_client.interaction_modal_builder("title", "custom", components=(component,)) + + assert result.type == 9 + assert isinstance(result, special_endpoints.InteractionModalBuilder) + assert result.components == [component] + def test_fetch_scheduled_event_users(self, rest_client: rest.RESTClientImpl): with mock.patch.object(special_endpoints, "ScheduledEventUserIterator") as iterator_cls: iterator = rest_client.fetch_scheduled_event_users( diff --git a/tests/hikari/interactions/test_component_interactions.py b/tests/hikari/interactions/test_component_interactions.py index ff9765f347..d96358c62d 100644 --- a/tests/hikari/interactions/test_component_interactions.py +++ b/tests/hikari/interactions/test_component_interactions.py @@ -136,3 +136,28 @@ def test_get_guild_when_cacheless(self, mock_component_interaction, mock_app): assert mock_component_interaction.get_guild() is None mock_app.cache.get_guild.assert_not_called() + + @pytest.mark.asyncio() + async def test_fetch_parent_message(self): + stub_interaction = hikari_test_helpers.mock_class_namespace( + component_interactions.ComponentInteraction, fetch_message=mock.AsyncMock(), init_=False + )() + stub_interaction.message = mock.Mock(id=3421) + + assert await stub_interaction.fetch_parent_message() is stub_interaction.fetch_message.return_value + + stub_interaction.fetch_message.assert_awaited_once_with(3421) + + def test_get_parent_message(self, mock_component_interaction, mock_app): + mock_component_interaction.message = mock.Mock(id=321655) + + assert mock_component_interaction.get_parent_message() is mock_app.cache.get_message.return_value + + mock_app.cache.get_message.assert_called_once_with(321655) + + def test_get_parent_message_when_cacheless(self, mock_component_interaction, mock_app): + mock_component_interaction.app = mock.Mock(traits.RESTAware) + + assert mock_component_interaction.get_parent_message() is None + + mock_app.cache.get_message.assert_not_called() diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py index 6f4612982f..7dc9fd3eb5 100644 --- a/tests/hikari/interactions/test_modal_interactions.py +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -29,7 +29,6 @@ from hikari.impl import special_endpoints from hikari.interactions import base_interactions from hikari.interactions import modal_interactions -from tests.hikari import hikari_test_helpers @pytest.fixture() From 4a13002cc871b898b65ae47e667d2679908a9375 Mon Sep 17 00:00:00 2001 From: HyperGH <46067571+HyperGH@users.noreply.github.com> Date: Thu, 7 Jul 2022 00:03:29 +0200 Subject: [PATCH 24/40] Fix typing --- hikari/impl/special_endpoints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 2b55f31a85..ab75e13f7f 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -1128,7 +1128,7 @@ def add_component( def build( self, entity_factory: entity_factory_.EntityFactory, / - ) -> typing.Tuple[data_binding.JSONObject, typing.Sequence[files.Resource[files.AsyncReader]]]: + ) -> typing.Tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]: data = data_binding.JSONObjectBuilder() data.put("title", self._title) data.put("custom_id", self._custom_id) @@ -1639,7 +1639,7 @@ def add_to_container(self) -> _ContainerProtoT: self._container.add_component(self) return self._container - def build(self) -> data_binding.JSONObject: + def build(self) -> typing.MutableMapping[str, typing.Any]: data = data_binding.JSONObjectBuilder() data["type"] = messages.ComponentType.TEXT_INPUT From 194a24d08bfb77395d87ff7041f74aecbc5a2562 Mon Sep 17 00:00:00 2001 From: HyperGH <46067571+HyperGH@users.noreply.github.com> Date: Thu, 7 Jul 2022 00:06:01 +0200 Subject: [PATCH 25/40] Adjust docstrings --- hikari/api/special_endpoints.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 831030fd55..cac7aedc0f 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -897,11 +897,6 @@ def set_title(self: _T, title: str, /) -> _T: ---------- title : builtins.str The title that will show up in the modal. - - Returns - ------- - InteractionModalBuilder - Object of this builder. """ @abc.abstractmethod @@ -912,11 +907,6 @@ def set_custom_id(self: _T, custom_id: str, /) -> _T: ---------- custom_id : builtins.str The developer set custom ID used for identifying interactions with this modal. - - Returns - ------- - InteractionModalBuilder - Object of this builder. """ @abc.abstractmethod @@ -927,11 +917,6 @@ def add_component(self: _T, component: ComponentBuilder, /) -> _T: ---------- component : ComponentBuilder The component builder to add to this modal. - - Returns - ------- - InteractionModalBuilder - Object of this builder. """ From dd88abdd84fb5abd398dea6292300eb035a57e46 Mon Sep 17 00:00:00 2001 From: HyperGH <46067571+HyperGH@users.noreply.github.com> Date: Thu, 7 Jul 2022 00:10:42 +0200 Subject: [PATCH 26/40] Add app_permissions --- hikari/impl/entity_factory.py | 3 +++ hikari/interactions/modal_interactions.py | 4 ++++ tests/hikari/interactions/test_modal_interactions.py | 1 + 3 files changed, 8 insertions(+) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index fd3dc7d4c6..9c052c3a5d 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -2140,6 +2140,8 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod member = None user = self.deserialize_user(payload["user"]) + app_perms = payload.get("app_permissions") + components: typing.List[typing.Any] = [] for component_payload in data_payload["components"]: try: @@ -2157,6 +2159,7 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod id=snowflakes.Snowflake(payload["id"]), type=base_interactions.InteractionType(payload["type"]), guild_id=guild_id, + app_permissions=permission_models.Permissions(app_perms) if app_perms is not None else None, guild_locale=locales.Locale(payload["guild_locale"]) if "guild_locale" in payload else None, locale=locales.Locale(payload["locale"]), channel_id=snowflakes.Snowflake(payload["channel_id"]), diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index a24b8190c2..16629d8970 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -38,6 +38,7 @@ from hikari import channels from hikari import guilds from hikari import messages +from hikari import permissions from hikari import snowflakes from hikari import traits from hikari.interactions import base_interactions @@ -119,6 +120,9 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes locale: str = attr.field(eq=False, hash=False, repr=True) """The selected language of the user who triggered this modal interaction.""" + app_permissions: typing.Optional[permissions.Permissions] = attr.field(eq=False, hash=False, repr=False) + """Permissions the bot has in this interaction's channel if it's in a guild.""" + components: typing.Sequence[messages.ActionRowComponent] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py index 7dc9fd3eb5..5adf37dc12 100644 --- a/tests/hikari/interactions/test_modal_interactions.py +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -54,6 +54,7 @@ def mock_modal_interaction(self, mock_app): message=object(), locale="es-ES", guild_locale="en-US", + app_permissions=543123, components=special_endpoints.ActionRowBuilder( components=[ modal_interactions.InteractionTextInput( From 8b98246c37462e576b363182a8444ff2dba818df Mon Sep 17 00:00:00 2001 From: sadru Date: Mon, 22 Aug 2022 18:46:24 +0200 Subject: [PATCH 27/40] drop ActionRowComponent in annotation --- hikari/impl/rate_limits.py | 2 +- hikari/interactions/modal_interactions.py | 2 +- hikari/internal/data_binding.py | 1 + tests/hikari/internal/test_routes.py | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/hikari/impl/rate_limits.py b/hikari/impl/rate_limits.py index 0ecd2c1544..4f49085805 100644 --- a/hikari/impl/rate_limits.py +++ b/hikari/impl/rate_limits.py @@ -498,7 +498,7 @@ def __init__( def __next__(self) -> float: """Get the next back off to sleep by.""" try: - value = self.base**self.increment + value = self.base ** self.increment if value >= self.maximum: value = self.maximum diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index 16629d8970..d226546230 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -123,7 +123,7 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes app_permissions: typing.Optional[permissions.Permissions] = attr.field(eq=False, hash=False, repr=False) """Permissions the bot has in this interaction's channel if it's in a guild.""" - components: typing.Sequence[messages.ActionRowComponent] = attr.field(eq=False, hash=False, repr=True) + components: typing.Sequence[messages.PartialComponent] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" async def fetch_channel(self) -> channels.TextableChannel: diff --git a/hikari/internal/data_binding.py b/hikari/internal/data_binding.py index 90017e8da1..17d9191f3b 100644 --- a/hikari/internal/data_binding.py +++ b/hikari/internal/data_binding.py @@ -90,6 +90,7 @@ def load_json(_: typing.AnyStr, /) -> typing.Union[JSONArray, JSONObject]: """Convert a JSON string to a Python type.""" raise NotImplementedError + else: import json diff --git a/tests/hikari/internal/test_routes.py b/tests/hikari/internal/test_routes.py index de5be53ef5..e68e791a3b 100644 --- a/tests/hikari/internal/test_routes.py +++ b/tests/hikari/internal/test_routes.py @@ -201,7 +201,7 @@ def test_passing_non_power_of_2_sizes_to_sizable_raises_ValueError(self, size): with pytest.raises(ValueError, match="size must be an integer power of 2 between 16 and 4096 inclusive"): route.compile("http://example.com", file_format="png", hash="boooob", size=size) - @pytest.mark.parametrize("size", [int(2**size) for size in [1, *range(17, 25)]]) + @pytest.mark.parametrize("size", [int(2 ** size) for size in [1, *range(17, 25)]]) def test_passing_invalid_magnitude_sizes_to_sizable_raises_ValueError(self, size): route = routes.CDNRoute("/foo/bar", {"png", "jpg", "png"}, sizable=True) with pytest.raises(ValueError, match="size must be an integer power of 2 between 16 and 4096 inclusive"): @@ -213,7 +213,7 @@ def test_passing_negative_sizes_to_sizable_raises_ValueError(self, size): with pytest.raises(ValueError, match="size must be positive"): route.compile("http://example.com", file_format="png", hash="boooob", size=size) - @pytest.mark.parametrize("size", [int(2**size) for size in range(4, 13)]) + @pytest.mark.parametrize("size", [int(2 ** size) for size in range(4, 13)]) def test_passing_valid_sizes_to_sizable_does_not_raise_ValueError(self, size): route = routes.CDNRoute("/foo/bar", {"png", "jpg", "gif"}, sizable=True) route.compile("http://example.com", file_format="png", hash="boooob", size=size) From c80b0f05980fc46e64ade4af6b6588d54774d4e4 Mon Sep 17 00:00:00 2001 From: sadru Date: Mon, 22 Aug 2022 19:00:37 +0200 Subject: [PATCH 28/40] format --- hikari/impl/rate_limits.py | 2 +- hikari/internal/data_binding.py | 1 - tests/hikari/internal/test_routes.py | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/hikari/impl/rate_limits.py b/hikari/impl/rate_limits.py index 4f49085805..0ecd2c1544 100644 --- a/hikari/impl/rate_limits.py +++ b/hikari/impl/rate_limits.py @@ -498,7 +498,7 @@ def __init__( def __next__(self) -> float: """Get the next back off to sleep by.""" try: - value = self.base ** self.increment + value = self.base**self.increment if value >= self.maximum: value = self.maximum diff --git a/hikari/internal/data_binding.py b/hikari/internal/data_binding.py index 17d9191f3b..90017e8da1 100644 --- a/hikari/internal/data_binding.py +++ b/hikari/internal/data_binding.py @@ -90,7 +90,6 @@ def load_json(_: typing.AnyStr, /) -> typing.Union[JSONArray, JSONObject]: """Convert a JSON string to a Python type.""" raise NotImplementedError - else: import json diff --git a/tests/hikari/internal/test_routes.py b/tests/hikari/internal/test_routes.py index e68e791a3b..de5be53ef5 100644 --- a/tests/hikari/internal/test_routes.py +++ b/tests/hikari/internal/test_routes.py @@ -201,7 +201,7 @@ def test_passing_non_power_of_2_sizes_to_sizable_raises_ValueError(self, size): with pytest.raises(ValueError, match="size must be an integer power of 2 between 16 and 4096 inclusive"): route.compile("http://example.com", file_format="png", hash="boooob", size=size) - @pytest.mark.parametrize("size", [int(2 ** size) for size in [1, *range(17, 25)]]) + @pytest.mark.parametrize("size", [int(2**size) for size in [1, *range(17, 25)]]) def test_passing_invalid_magnitude_sizes_to_sizable_raises_ValueError(self, size): route = routes.CDNRoute("/foo/bar", {"png", "jpg", "png"}, sizable=True) with pytest.raises(ValueError, match="size must be an integer power of 2 between 16 and 4096 inclusive"): @@ -213,7 +213,7 @@ def test_passing_negative_sizes_to_sizable_raises_ValueError(self, size): with pytest.raises(ValueError, match="size must be positive"): route.compile("http://example.com", file_format="png", hash="boooob", size=size) - @pytest.mark.parametrize("size", [int(2 ** size) for size in range(4, 13)]) + @pytest.mark.parametrize("size", [int(2**size) for size in range(4, 13)]) def test_passing_valid_sizes_to_sizable_does_not_raise_ValueError(self, size): route = routes.CDNRoute("/foo/bar", {"png", "jpg", "gif"}, sizable=True) route.compile("http://example.com", file_format="png", hash="boooob", size=size) From 844cc988ad39cfac82f023535a652cce926f19bc Mon Sep 17 00:00:00 2001 From: sadru Date: Mon, 22 Aug 2022 19:03:55 +0200 Subject: [PATCH 29/40] Fix create_modal_response docstring --- hikari/interactions/base_interactions.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/hikari/interactions/base_interactions.py b/hikari/interactions/base_interactions.py index 3fe7413f92..84d8b58eeb 100644 --- a/hikari/interactions/base_interactions.py +++ b/hikari/interactions/base_interactions.py @@ -592,13 +592,6 @@ async def create_modal_response( Parameters ---------- - interaction : hikari.snowflakes.SnowflakeishOr[hikari.interactions.base_interactions.PartialInteraction] - Object or ID of the interaction this response is for. - token : builtins.str - The command interaction's token. - - Other Parameters - ---------------- title : str The title that will show up in the modal. custom_id : str From ec79d79d590cde8ee2c7a643cc9653c3e3a7a95b Mon Sep 17 00:00:00 2001 From: sadru Date: Mon, 22 Aug 2022 19:05:40 +0200 Subject: [PATCH 30/40] Typo in docstring --- hikari/api/special_endpoints.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index cac7aedc0f..57642cea0a 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -1604,7 +1604,7 @@ def min_length(self) -> undefined.UndefinedOr[int]: @property @abc.abstractmethod def max_length(self) -> undefined.UndefinedOr[int]: - """Maxmimum length the text should have.""" + """Maximum length the text should have.""" @abc.abstractmethod def set_style(self: _T, style: typing.Union[messages.TextInputStyle, int], /) -> _T: @@ -1713,12 +1713,12 @@ def set_min_length(self: _T, min_length: int, /) -> _T: @abc.abstractmethod def set_max_length(self: _T, max_length: int, /) -> _T: - """Set the maxmimum length the text should have. + """Set the maximum length the text should have. Parameters ---------- max_length : builtins.int - The maxmimum length the text should have. + The maximum length the text should have. Returns ------- From 52e794810c5f41c82b083746b1e7a47053765c07 Mon Sep 17 00:00:00 2001 From: sadru Date: Mon, 22 Aug 2022 22:32:50 +0200 Subject: [PATCH 31/40] Migrate to separate modal components --- hikari/api/rest.py | 10 +++ hikari/api/special_endpoints.py | 43 +++++++++- hikari/impl/entity_factory.py | 43 ++++++++-- hikari/impl/rest.py | 3 + hikari/impl/special_endpoints.py | 53 ++++++++++-- hikari/interactions/modal_interactions.py | 82 ++++++++++++++++++- hikari/messages.py | 18 ---- tests/hikari/impl/test_entity_factory.py | 4 +- tests/hikari/impl/test_rest.py | 4 +- tests/hikari/impl/test_special_endpoints.py | 27 +++--- .../interactions/test_modal_interactions.py | 3 +- 11 files changed, 236 insertions(+), 54 deletions(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 15fb78cba9..5d3c844330 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -7818,6 +7818,16 @@ def build_action_row(self) -> special_endpoints.ActionRowBuilder: The initialised action row builder. """ + @abc.abstractmethod + def build_modal_action_row(self) -> special_endpoints.ModalActionRowBuilder: + """Build an action row modal component for use in interactions and REST calls. + + Returns + ------- + hikari.api.special_endpoints.ModalActionRowBuilder + The initialised action row builder. + """ + @abc.abstractmethod async def fetch_scheduled_event( self, diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index c9de331cc8..35ea0df4ae 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -42,6 +42,7 @@ "SelectOptionBuilder", "TextInputBuilder", "InteractionModalBuilder", + "ModalActionRowBuilder", ) import abc @@ -68,6 +69,7 @@ from hikari.api import entity_factory as entity_factory_ from hikari.api import rest as rest_api from hikari.interactions import base_interactions + from hikari.interactions import modal_interactions from hikari.internal import time _T = typing.TypeVar("_T") @@ -1627,7 +1629,7 @@ def label(self) -> str: @property @abc.abstractmethod - def style(self) -> messages.TextInputStyle: + def style(self) -> modal_interactions.TextInputStyle: """Style to use for the text input.""" @property @@ -1656,12 +1658,12 @@ def max_length(self) -> undefined.UndefinedOr[int]: """Maximum length the text should have.""" @abc.abstractmethod - def set_style(self: _T, style: typing.Union[messages.TextInputStyle, int], /) -> _T: + def set_style(self: _T, style: typing.Union[modal_interactions.TextInputStyle, int], /) -> _T: """Set the style to use for the text input. Parameters ---------- - style : typing.Union[hikari.messages.TextInputStyle, int] + style : typing.Union[hikari.modal_interactions.TextInputStyle, int] Style to use for the text input. Returns @@ -1882,6 +1884,41 @@ def add_select_menu(self: _T, custom_id: str, /) -> SelectMenuBuilder[_T]: component. """ + +class ModalActionRowBuilder(ComponentBuilder, abc.ABC): + """Builder class for modal action row components.""" + + __slots__: typing.Sequence[str] = () + + @property + @abc.abstractmethod + def components(self) -> typing.Sequence[ComponentBuilder]: + """Sequence of the component builders registered within this action row.""" + + @abc.abstractmethod + def add_component( + self: _T, + component: ComponentBuilder, + /, + ) -> _T: + """Add a component to this action row builder. + + !!! warning + It is generally better to use `ActionRowBuilder.add_button` + and `ActionRowBuilder.add_select_menu` to add your + component to the builder. Those methods utilize this one. + + Parameters + ---------- + component : ComponentBuilder + The component builder to add to the action row. + + Returns + ------- + ActionRowBuilder + The builder object to enable chained calls. + """ + @abc.abstractmethod def add_text_input( self: _T, diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index a032a7411a..43f671620a 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -409,6 +409,7 @@ class EntityFactoryImpl(entity_factory.EntityFactory): "_audit_log_entry_converters", "_audit_log_event_mapping", "_command_mapping", + "_modal_component_type_mapping", "_component_type_mapping", "_dm_channel_type_mapping", "_guild_channel_type_mapping", @@ -482,7 +483,10 @@ def __init__(self, app: traits.RESTAware) -> None: message_models.ComponentType.ACTION_ROW: self._deserialize_action_row, message_models.ComponentType.BUTTON: self._deserialize_button, message_models.ComponentType.SELECT_MENU: self._deserialize_select_menu, - message_models.ComponentType.TEXT_INPUT: self._deserialize_text_input, + } + self._modal_component_type_mapping = { + modal_interactions.ModalComponentType.ACTION_ROW: self._deserialize_modal_action_row, + modal_interactions.ModalComponentType.TEXT_INPUT: self._deserialize_text_input, } self._dm_channel_type_mapping = { channel_models.ChannelType.DM: self.deserialize_dm, @@ -2200,10 +2204,10 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod app_perms = payload.get("app_permissions") - components: typing.List[typing.Any] = [] + components: typing.List[modal_interactions.PartialModalComponent] = [] for component_payload in data_payload["components"]: try: - components.append(self._deserialize_component(component_payload)) + components.append(self._deserialize_modal_component(component_payload)) except errors.UnrecognisedEntityError: pass @@ -2420,6 +2424,31 @@ def _deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_ is_disabled=payload.get("disabled", False), ) + def _deserialize_component(self, payload: data_binding.JSONObject) -> message_models.PartialComponent: + component_type = message_models.ComponentType(payload["type"]) + + if deserialize := self._component_type_mapping.get(component_type): + return deserialize(payload) + + _LOGGER.debug("Unknown component type %s", component_type) + raise errors.UnrecognisedEntityError(f"Unrecognised component type {component_type}") + + def _deserialize_modal_action_row( + self, payload: data_binding.JSONObject + ) -> modal_interactions.ModalActionRowComponent: + components: typing.List[modal_interactions.PartialModalComponent] = [] + + for component_payload in payload["components"]: + try: + components.append(self._deserialize_modal_component(component_payload)) + + except errors.UnrecognisedEntityError: + pass + + return modal_interactions.ModalActionRowComponent( + type=message_models.ComponentType(payload["type"]), components=components + ) + def _deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.InteractionTextInput: return modal_interactions.InteractionTextInput( type=message_models.ComponentType(payload["type"]), @@ -2427,10 +2456,12 @@ def _deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_int value=payload["value"], ) - def _deserialize_component(self, payload: data_binding.JSONObject) -> message_models.PartialComponent: - component_type = message_models.ComponentType(payload["type"]) + def _deserialize_modal_component( + self, payload: data_binding.JSONObject + ) -> modal_interactions.PartialModalComponent: + component_type = modal_interactions.ModalComponentType(payload["type"]) - if deserialize := self._component_type_mapping.get(component_type): + if deserialize := self._modal_component_type_mapping.get(component_type): return deserialize(payload) _LOGGER.debug("Unknown component type %s", component_type) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 2722371ad0..3958a2d9cf 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -3594,6 +3594,9 @@ async def create_modal_response( def build_action_row(self) -> special_endpoints.ActionRowBuilder: return special_endpoints_impl.ActionRowBuilder() + def build_modal_action_row(self) -> special_endpoints.ModalActionRowBuilder: + return special_endpoints_impl.ModalActionRowBuilder() + async def fetch_scheduled_event( self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild], diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 87a3577eb2..59c1fb05bd 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -41,6 +41,7 @@ "SelectMenuBuilder", "TextInputBuilder", "InteractionModalBuilder", + "ModalActionRowBuilder", ) import asyncio @@ -60,6 +61,7 @@ from hikari import undefined from hikari.api import special_endpoints from hikari.interactions import base_interactions +from hikari.interactions import modal_interactions from hikari.internal import attr_extensions from hikari.internal import data_binding from hikari.internal import mentions @@ -92,6 +94,7 @@ ) _InteractionModalBuilderT = typing.TypeVar("_InteractionModalBuilderT", bound="InteractionModalBuilder") _ActionRowBuilderT = typing.TypeVar("_ActionRowBuilderT", bound="ActionRowBuilder") + _ModalActionRowBuilderT = typing.TypeVar("_ModalActionRowBuilderT", bound="ModalActionRowBuilder") _ButtonBuilderT = typing.TypeVar("_ButtonBuilderT", bound="_ButtonBuilder[typing.Any]") _SelectOptionBuilderT = typing.TypeVar("_SelectOptionBuilderT", bound="_SelectOptionBuilder[typing.Any]") _SelectMenuBuilderT = typing.TypeVar("_SelectMenuBuilderT", bound="SelectMenuBuilder[typing.Any]") @@ -1605,7 +1608,7 @@ class TextInputBuilder(special_endpoints.TextInputBuilder[_ContainerProtoT]): _custom_id: str = attr.field() _label: str = attr.field() - _style: messages.TextInputStyle = attr.field(default=messages.TextInputStyle.SHORT) + _style: modal_interactions.TextInputStyle = attr.field(default=modal_interactions.TextInputStyle.SHORT) _placeholder: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED, kw_only=True) _value: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED, kw_only=True) _required: undefined.UndefinedOr[bool] = attr.field(default=undefined.UNDEFINED, kw_only=True) @@ -1621,7 +1624,7 @@ def label(self) -> str: return self._label @property - def style(self) -> messages.TextInputStyle: + def style(self) -> modal_interactions.TextInputStyle: return self._style @property @@ -1644,8 +1647,10 @@ def min_length(self) -> undefined.UndefinedOr[int]: def max_length(self) -> undefined.UndefinedOr[int]: return self._max_length - def set_style(self: _TextInputBuilderT, style: typing.Union[messages.TextInputStyle, int], /) -> _TextInputBuilderT: - self._style = messages.TextInputStyle(style) + def set_style( + self: _TextInputBuilderT, style: typing.Union[modal_interactions.TextInputStyle, int], / + ) -> _TextInputBuilderT: + self._style = modal_interactions.TextInputStyle(style) return self def set_custom_id(self: _TextInputBuilderT, custom_id: str, /) -> _TextInputBuilderT: @@ -1683,7 +1688,7 @@ def add_to_container(self) -> _ContainerProtoT: def build(self) -> typing.MutableMapping[str, typing.Any]: data = data_binding.JSONObjectBuilder() - data["type"] = messages.ComponentType.TEXT_INPUT + data["type"] = modal_interactions.ModalComponentType.TEXT_INPUT data["style"] = self._style data["custom_id"] = self._custom_id data["label"] = self._label @@ -1761,12 +1766,44 @@ def add_select_menu( self._assert_can_add_type(messages.ComponentType.SELECT_MENU) return SelectMenuBuilder(container=self, custom_id=custom_id) + def build(self) -> typing.MutableMapping[str, typing.Any]: + return { + "type": messages.ComponentType.ACTION_ROW, + "components": [component.build() for component in self._components], + } + + +@attr.define(kw_only=True, weakref_slot=False) +class ModalActionRowBuilder(special_endpoints.ModalActionRowBuilder): + """Standard implementation of `hikari.api.special_endpoints.ActionRowBuilder`.""" + + _components: typing.List[special_endpoints.ComponentBuilder] = attr.field(factory=list) + _stored_type: typing.Optional[modal_interactions.ModalComponentType] = attr.field(default=None) + + @property + def components(self) -> typing.Sequence[special_endpoints.ComponentBuilder]: + return self._components.copy() + + def _assert_can_add_type(self, type_: modal_interactions.ModalComponentType, /) -> None: + if self._stored_type is not None and self._stored_type != type_: + raise ValueError( + f"{type_} component type cannot be added to a container which already holds {self._stored_type}" + ) + + self._stored_type = type_ + + def add_component( + self: _ModalActionRowBuilderT, component: special_endpoints.ComponentBuilder, / + ) -> _ModalActionRowBuilderT: + self._components.append(component) + return self + def add_text_input( - self: _ActionRowBuilderT, + self: _ModalActionRowBuilderT, custom_id: str, label: str, - ) -> special_endpoints.TextInputBuilder[_ActionRowBuilderT]: - self._assert_can_add_type(messages.ComponentType.TEXT_INPUT) + ) -> special_endpoints.TextInputBuilder[_ModalActionRowBuilderT]: + self._assert_can_add_type(modal_interactions.ModalComponentType.TEXT_INPUT) return TextInputBuilder(container=self, custom_id=custom_id, label=label) def build(self) -> typing.MutableMapping[str, typing.Any]: diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index d226546230..0a1bf035d5 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -29,6 +29,10 @@ "ModalInteraction", "InteractionTextInput", "ModalInteraction", + "TextInputStyle", + "ModalComponentType", + "PartialModalComponent", + "ModalActionRowComponent", ] import typing @@ -43,6 +47,7 @@ from hikari import traits from hikari.interactions import base_interactions from hikari.internal import attr_extensions +from hikari.internal import enums if typing.TYPE_CHECKING: from hikari import users as _users @@ -60,8 +65,81 @@ """ +@typing.final +class ModalComponentType(int, enums.Enum): + """Types of components found within Discord.""" + + ACTION_ROW = 1 + """A non-interactive container component for other types of components. + + !!! note + As this is a container component it can never be contained within another + component and therefore will always be top-level. + + !!! note + As of writing this can only contain one component type. + """ + + TEXT_INPUT = 4 + """A text input component + + !! note + This component may only be used inside a modal container. + """ + + +class TextInputStyle(int, enums.Enum): + """A text input style.""" + + SHORT = 1 + """Intended for short single-line text.""" + + PARAGRAPH = 2 + """Intended for much longer inputs.""" + + +@attr.define(kw_only=True, weakref_slot=False) +class PartialModalComponent: + """Base class for all model component entities.""" + + type: typing.Union[ModalComponentType, int] = attr.field() + """The type of component this is.""" + + +@attr.define(weakref_slot=False) +class ModalActionRowComponent(PartialModalComponent): + """Represents a row of components attached to a message. + + !!! note + This is a top-level container component and will never be found within + another component. + """ + + components: typing.Sequence[PartialModalComponent] = attr.field() + """Sequence of the components contained within this row.""" + + @typing.overload + def __getitem__(self, index: int, /) -> PartialModalComponent: + ... + + @typing.overload + def __getitem__(self, slice_: slice, /) -> typing.Sequence[PartialModalComponent]: + ... + + def __getitem__( + self, index_or_slice: typing.Union[int, slice], / + ) -> typing.Union[PartialModalComponent, typing.Sequence[PartialModalComponent]]: + return self.components[index_or_slice] + + def __iter__(self) -> typing.Iterator[PartialModalComponent]: + return iter(self.components) + + def __len__(self) -> int: + return len(self.components) + + @attr.define(kw_only=True, weakref_slot=False) -class InteractionTextInput(messages.PartialComponent): +class InteractionTextInput(PartialModalComponent): """A text input component in a modal interaction.""" custom_id: str = attr.field(repr=True) @@ -123,7 +201,7 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes app_permissions: typing.Optional[permissions.Permissions] = attr.field(eq=False, hash=False, repr=False) """Permissions the bot has in this interaction's channel if it's in a guild.""" - components: typing.Sequence[messages.PartialComponent] = attr.field(eq=False, hash=False, repr=True) + components: typing.Sequence[PartialModalComponent] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" async def fetch_channel(self) -> channels.TextableChannel: diff --git a/hikari/messages.py b/hikari/messages.py index 09437ee637..cc647e9865 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -39,7 +39,6 @@ "ActionRowComponent", "ButtonComponent", "ButtonStyle", - "TextInputStyle", "SelectMenuOption", "SelectMenuComponent", "InteractiveButtonTypes", @@ -507,13 +506,6 @@ class ComponentType(int, enums.Enum): as `ComponentType.ACTION_ROW`. """ - TEXT_INPUT = 4 - """A text input component - - !! note - This component may only be used inside a modal container. - """ - @typing.final class ButtonStyle(int, enums.Enum): @@ -578,16 +570,6 @@ class ButtonStyle(int, enums.Enum): """ -class TextInputStyle(int, enums.Enum): - """A text input style.""" - - SHORT = 1 - """Intended for short single-line text.""" - - PARAGRAPH = 2 - """Intended for much longer inputs.""" - - @attr.define(kw_only=True, weakref_slot=False) class PartialComponent: """Base class for all component entities.""" diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 9b01d60a6e..860da79119 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -3989,11 +3989,11 @@ def test_deserialize_modal_interaction( assert isinstance(interaction, modal_interactions.ModalInteraction) short_action_row = interaction.components[0] - assert isinstance(short_action_row, message_models.ActionRowComponent) + assert isinstance(short_action_row, modal_interactions.ModalActionRowComponent) short_text_input = short_action_row.components[0] assert isinstance(short_text_input, modal_interactions.InteractionTextInput) assert short_text_input.value == "Wumpus" - assert short_text_input.type == message_models.ComponentType.TEXT_INPUT + assert short_text_input.type == modal_interactions.ModalComponentType.TEXT_INPUT assert short_text_input.custom_id == "name" def test_deserialize_modal_interaction_with_user( diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 135a6a8afb..3bcee1b760 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -1423,7 +1423,9 @@ def test_interaction_message_builder(self, rest_client): def test_interaction_modal_builder(self, rest_client): result = rest_client.interaction_modal_builder("title", "custom") - result.add_component(special_endpoints.ActionRowBuilder().add_text_input("idd", "labell").add_to_container()) + result.add_component( + special_endpoints.ModalActionRowBuilder().add_text_input("idd", "labell").add_to_container() + ) assert result.type == 9 assert isinstance(result, special_endpoints.InteractionModalBuilder) diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py index 263cf97a58..87fd0885e0 100644 --- a/tests/hikari/impl/test_special_endpoints.py +++ b/tests/hikari/impl/test_special_endpoints.py @@ -33,6 +33,7 @@ from hikari import undefined from hikari.impl import special_endpoints from hikari.interactions import base_interactions +from hikari.interactions import modal_interactions from hikari.internal import routes from tests.hikari import hikari_test_helpers @@ -1221,8 +1222,8 @@ def text_input(self): ) def test_set_style(self, text_input): - assert text_input.set_style(messages.TextInputStyle.PARAGRAPH) is text_input - assert text_input.style == messages.TextInputStyle.PARAGRAPH + assert text_input.set_style(modal_interactions.TextInputStyle.PARAGRAPH) is text_input + assert text_input.style == modal_interactions.TextInputStyle.PARAGRAPH def test_set_custom_id(self, text_input): assert text_input.set_custom_id("custooom") is text_input @@ -1264,7 +1265,7 @@ def test_build(self): ).build() assert result == { - "type": messages.ComponentType.TEXT_INPUT, + "type": modal_interactions.ModalComponentType.TEXT_INPUT, "style": 1, "custom_id": "o2o2o2", "label": "label", @@ -1286,7 +1287,7 @@ def test_build_partial(self): ) assert result == { - "type": messages.ComponentType.TEXT_INPUT, + "type": modal_interactions.ModalComponentType.TEXT_INPUT, "style": 1, "custom_id": "o2o2o2", "label": "label", @@ -1328,14 +1329,6 @@ def test_add_select_menu(self): assert row.components == [menu] - def test_add_text_input(self): - row = special_endpoints.ActionRowBuilder() - menu = row.add_text_input("hihihi", "label") - - menu.add_to_container() - - assert row.components == [menu] - def test_build(self): mock_component_1 = mock.Mock() mock_component_2 = mock.Mock() @@ -1351,3 +1344,13 @@ def test_build(self): } mock_component_1.build.assert_called_once_with() mock_component_2.build.assert_called_once_with() + + +class TestModalActionRow: + def test_add_text_input(self): + row = special_endpoints.ModalActionRowBuilder() + menu = row.add_text_input("hihihi", "label") + + menu.add_to_container() + + assert row.components == [menu] diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py index 5adf37dc12..6330a6ebae 100644 --- a/tests/hikari/interactions/test_modal_interactions.py +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -23,7 +23,6 @@ import pytest from hikari import channels -from hikari import messages from hikari import snowflakes from hikari import traits from hikari.impl import special_endpoints @@ -58,7 +57,7 @@ def mock_modal_interaction(self, mock_app): components=special_endpoints.ActionRowBuilder( components=[ modal_interactions.InteractionTextInput( - type=messages.ComponentType.TEXT_INPUT, custom_id="le id", value="le value" + type=modal_interactions.ModalComponentType.TEXT_INPUT, custom_id="le id", value="le value" ) ], ), From 3338f586a70f35c8edb7e556ba2faba17fa5b8fe Mon Sep 17 00:00:00 2001 From: davfsa Date: Fri, 23 Sep 2022 21:07:09 +0200 Subject: [PATCH 32/40] Fix issues with rebase --- hikari/interactions/component_interactions.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hikari/interactions/component_interactions.py b/hikari/interactions/component_interactions.py index 7f391bbcf4..eda34469c0 100644 --- a/hikari/interactions/component_interactions.py +++ b/hikari/interactions/component_interactions.py @@ -324,7 +324,6 @@ def get_guild(self) -> typing.Optional[guilds.GatewayGuild]: return None - @deprecation.deprecated("2.0.0.dev110", "fetch_message") async def fetch_parent_message(self) -> messages.Message: """Fetch the message which this interaction was triggered on. @@ -355,9 +354,13 @@ async def fetch_parent_message(self) -> messages.Message: hikari.errors.InternalServerError If an internal error occurs on Discord while handling the request. """ + deprecation.warn_deprecated( + "fetch_parent_message", + removal_version="2.0.0.dev113", + additional_info="The message can be accessed through the 'message' attribute", + ) return await self.fetch_message(self.message.id) - @deprecation.deprecated("2.0.0.dev110", "message") def get_parent_message(self) -> typing.Optional[messages.PartialMessage]: """Get the message which this interaction was triggered on from the cache. @@ -366,6 +369,11 @@ def get_parent_message(self) -> typing.Optional[messages.PartialMessage]: typing.Optional[hikari.messages.Message] The object of the message found in the cache or `builtins.None`. """ + deprecation.warn_deprecated( + "get_parent_message", + removal_version="2.0.0.dev113", + additional_info="The message can be accessed through the 'message' attribute", + ) if isinstance(self.app, traits.CacheAware): return self.app.cache.get_message(self.message.id) From 04a69229806df7e3dddfb79419547e647dc3d852 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sat, 29 Oct 2022 15:24:06 +0200 Subject: [PATCH 33/40] Finish modals off --- changes/1002.deprecation.md | 1 + hikari/__init__.py | 1 + hikari/__init__.pyi | 1 + hikari/api/interaction_server.py | 12 +- hikari/api/rest.py | 34 +- hikari/api/special_endpoints.py | 22 +- hikari/components.py | 292 ++++++++++++++++++ hikari/impl/entity_factory.py | 108 +++---- hikari/impl/interaction_server.py | 12 +- hikari/impl/rest.py | 50 +-- hikari/impl/rest_bot.py | 10 +- hikari/impl/special_endpoints.py | 82 ++--- hikari/interactions/base_interactions.py | 30 +- hikari/interactions/component_interactions.py | 3 +- hikari/interactions/modal_interactions.py | 106 +------ hikari/internal/cache.py | 3 +- hikari/messages.py | 244 +-------------- tests/hikari/impl/test_entity_factory.py | 24 +- tests/hikari/impl/test_rest.py | 12 +- tests/hikari/impl/test_special_endpoints.py | 54 ++-- .../interactions/test_base_interactions.py | 11 +- .../interactions/test_modal_interactions.py | 7 +- tests/hikari/test_components.py | 49 +++ tests/hikari/test_messages.py | 27 -- 24 files changed, 599 insertions(+), 596 deletions(-) create mode 100644 changes/1002.deprecation.md create mode 100644 hikari/components.py create mode 100644 tests/hikari/test_components.py diff --git a/changes/1002.deprecation.md b/changes/1002.deprecation.md new file mode 100644 index 0000000000..240d7a839a --- /dev/null +++ b/changes/1002.deprecation.md @@ -0,0 +1 @@ +Deprecate `RESTClientImpl.build_action_row` in favour of `RESTClientImpl.build_message_action_row`. diff --git a/hikari/__init__.py b/hikari/__init__.py index 020e2609f6..10b95e050a 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -71,6 +71,7 @@ from hikari.colors import * from hikari.colours import * from hikari.commands import * +from hikari.components import * from hikari.embeds import * from hikari.emojis import * from hikari.errors import * diff --git a/hikari/__init__.pyi b/hikari/__init__.pyi index c29f5f9c60..3d59a211ff 100644 --- a/hikari/__init__.pyi +++ b/hikari/__init__.pyi @@ -44,6 +44,7 @@ from hikari.channels import * from hikari.colors import * from hikari.colours import * from hikari.commands import * +from hikari.components import * from hikari.embeds import * from hikari.emojis import * from hikari.errors import * diff --git a/hikari/api/interaction_server.py b/hikari/api/interaction_server.py index 2ee99b9595..ef6e80968f 100644 --- a/hikari/api/interaction_server.py +++ b/hikari/api/interaction_server.py @@ -42,7 +42,7 @@ special_endpoints.InteractionDeferredBuilder, special_endpoints.InteractionMessageBuilder, ] - _ModalResponseBuilder = typing.Union[ + _ModalOrMessageResponseBuilder = typing.Union[ _MessageResponseBuilderT, special_endpoints.InteractionModalBuilder, ] @@ -142,14 +142,14 @@ async def on_interaction(self, body: bytes, signature: bytes, timestamp: bytes) @abc.abstractmethod def get_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], / - ) -> typing.Optional[ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilder]]: + ) -> typing.Optional[ListenerT[command_interactions.CommandInteraction, _ModalOrMessageResponseBuilder]]: ... @typing.overload @abc.abstractmethod def get_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], / - ) -> typing.Optional[ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilder]]: + ) -> typing.Optional[ListenerT[component_interactions.ComponentInteraction, _ModalOrMessageResponseBuilder]]: ... @typing.overload @@ -198,7 +198,7 @@ def get_listener( def set_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], - listener: typing.Optional[ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilder]], + listener: typing.Optional[ListenerT[command_interactions.CommandInteraction, _ModalOrMessageResponseBuilder]], /, *, replace: bool = False, @@ -210,7 +210,9 @@ def set_listener( def set_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], - listener: typing.Optional[ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilder]], + listener: typing.Optional[ + ListenerT[component_interactions.ComponentInteraction, _ModalOrMessageResponseBuilder] + ], /, *, replace: bool = False, diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 642d5f76f9..4e6003e6aa 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -7955,13 +7955,7 @@ def interaction_message_builder( """ @abc.abstractmethod - def interaction_modal_builder( - self, - title: str, - custom_id: str, - *, - components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, - ) -> special_endpoints.InteractionModalBuilder: + def interaction_modal_builder(self, title: str, custom_id: str) -> special_endpoints.InteractionModalBuilder: """Create a builder for a modal interaction response. Parameters @@ -7970,8 +7964,6 @@ def interaction_modal_builder( The title that will show up in the modal. custom_id : builtins.str Developer set custom ID used for identifying interactions with this modal. - components : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] - Sequence of component builders to send in this modal. Returns ------- @@ -8387,7 +8379,8 @@ async def create_modal_response( *, title: str, custom_id: str, - components: typing.Sequence[special_endpoints.ComponentBuilder], + component: undefined.UndefinedOr[special_endpoints.ComponentBuilder] = undefined.UNDEFINED, + components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, ) -> None: """Create a response by sending a modal. @@ -8397,24 +8390,31 @@ async def create_modal_response( Object or ID of the interaction this response is for. token : builtins.str The command interaction's token. - - Other Parameters - ---------------- title : str The title that will show up in the modal. custom_id : str Developer set custom ID used for identifying interactions with this modal. - components : typing.Sequence[special_endpoints.ComponentBuilder] + + Other Parameters + ---------------- + component : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] + A component builders to send in this modal. + components : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] A sequence of component builders to send in this modal. + + Raises + ------ + ValueError + If both `component` and `components` are specified or if none are specified. """ @abc.abstractmethod - def build_action_row(self) -> special_endpoints.ActionRowBuilder: - """Build an action row message component for use in message create and REST calls. + def build_message_action_row(self) -> special_endpoints.MessageActionRowBuilder: + """Build a message action row message component for use in message create and REST calls. Returns ------- - hikari.api.special_endpoints.ActionRowBuilder + hikari.api.special_endpoints.MessageActionRowBuilder The initialised action row builder. """ diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 8cfc9d1875..2fc38966b9 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -24,7 +24,6 @@ from __future__ import annotations __all__: typing.Sequence[str] = ( - "ActionRowBuilder", "ButtonBuilder", "CommandBuilder", "SlashCommandBuilder", @@ -42,6 +41,7 @@ "SelectOptionBuilder", "TextInputBuilder", "InteractionModalBuilder", + "MessageActionRowBuilder", "ModalActionRowBuilder", ) @@ -56,6 +56,7 @@ from hikari import channels from hikari import colors from hikari import commands + from hikari import components as components_ from hikari import embeds as embeds_ from hikari import emojis from hikari import files @@ -69,7 +70,6 @@ from hikari.api import entity_factory as entity_factory_ from hikari.api import rest as rest_api from hikari.interactions import base_interactions - from hikari.interactions import modal_interactions from hikari.internal import time _T = typing.TypeVar("_T") @@ -1264,7 +1264,7 @@ class ButtonBuilder(ComponentBuilder, abc.ABC, typing.Generic[_ContainerT]): @property @abc.abstractmethod - def style(self) -> typing.Union[messages.ButtonStyle, int]: + def style(self) -> typing.Union[components_.ButtonStyle, int]: """Button's style.""" @property @@ -1642,7 +1642,7 @@ def label(self) -> str: @property @abc.abstractmethod - def style(self) -> modal_interactions.TextInputStyle: + def style(self) -> components_.TextInputStyle: """Style to use for the text input.""" @property @@ -1671,7 +1671,7 @@ def max_length(self) -> undefined.UndefinedOr[int]: """Maximum length the text should have.""" @abc.abstractmethod - def set_style(self: _T, style: typing.Union[modal_interactions.TextInputStyle, int], /) -> _T: + def set_style(self: _T, style: typing.Union[components_.TextInputStyle, int], /) -> _T: """Set the style to use for the text input. Parameters @@ -1801,7 +1801,7 @@ def add_to_container(self) -> _ContainerT: """ -class ActionRowBuilder(ComponentBuilder, abc.ABC): +class MessageActionRowBuilder(ComponentBuilder, abc.ABC): """Builder class for action row components.""" __slots__: typing.Sequence[str] = () @@ -1838,25 +1838,27 @@ def add_component( @typing.overload @abc.abstractmethod def add_button( - self: _T, style: messages.InteractiveButtonTypesT, custom_id: str, / + self: _T, style: components_.InteractiveButtonTypesT, custom_id: str, / ) -> InteractiveButtonBuilder[_T]: ... @typing.overload @abc.abstractmethod - def add_button(self: _T, style: typing.Literal[messages.ButtonStyle.LINK, 5], url: str, /) -> LinkButtonBuilder[_T]: + def add_button( + self: _T, style: typing.Literal[components_.ButtonStyle.LINK, 5], url: str, / + ) -> LinkButtonBuilder[_T]: ... @typing.overload @abc.abstractmethod def add_button( - self: _T, style: typing.Union[int, messages.ButtonStyle], url_or_custom_id: str, / + self: _T, style: typing.Union[int, components_.ButtonStyle], url_or_custom_id: str, / ) -> typing.Union[LinkButtonBuilder[_T], InteractiveButtonBuilder[_T]]: ... @abc.abstractmethod def add_button( - self: _T, style: typing.Union[int, messages.ButtonStyle], url_or_custom_id: str, / + self: _T, style: typing.Union[int, components_.ButtonStyle], url_or_custom_id: str, / ) -> typing.Union[LinkButtonBuilder[_T], InteractiveButtonBuilder[_T]]: """Add a button component to this action row builder. diff --git a/hikari/components.py b/hikari/components.py new file mode 100644 index 0000000000..60e9926aa4 --- /dev/null +++ b/hikari/components.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Application and entities that are used to describe components on Discord.""" + +from __future__ import annotations + +__all__: typing.Sequence[str] = ( + "ComponentType", + "PartialComponent", + "ActionRowComponent", + "ButtonStyle", + "ButtonComponent", + "SelectMenuOption", + "SelectMenuComponent", + "TextInputStyle", + "TextInputComponent", + "InteractiveButtonTypes", + "InteractiveButtonTypesT", +) + +import typing + +import attr + +from hikari import emojis +from hikari.internal import enums + + +@typing.final +class ComponentType(int, enums.Enum): + """Types of components found within Discord.""" + + ACTION_ROW = 1 + """A non-interactive container component for other types of components. + + !!! note + As this is a container component it can never be contained within another + component and therefore will always be top-level. + + !!! note + As of writing this can only contain one component type. + """ + + BUTTON = 2 + """A button component. + + !!! note + This cannot be top-level and must be within a container component such + as `ComponentType.ACTION_ROW`. + """ + + SELECT_MENU = 3 + """A select menu component. + + !!! note + This cannot be top-level and must be within a container component such + as `ComponentType.ACTION_ROW`. + """ + + TEXT_INPUT = 4 + """A text input component. + + !! note + This component may only be used inside a modal container. + + !!! 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): + """Enum of the available button styles. + + More information, such as how these look, can be found at + https://discord.com/developers/docs/interactions/message-components#buttons-button-styles + """ + + PRIMARY = 1 + """A blurple "call to action" button.""" + + SECONDARY = 2 + """A grey neutral button.""" + + SUCCESS = 3 + """A green button.""" + + DANGER = 4 + """A red button (usually indicates a destructive action).""" + + LINK = 5 + """A grey button which navigates to a URL. + + !!! warning + Unlike the other button styles, clicking this one will not trigger an + interaction and custom_id shouldn't be included for this style. + """ + + +@typing.final +class TextInputStyle(int, enums.Enum): + """A text input style.""" + + SHORT = 1 + """Intended for short single-line text.""" + + PARAGRAPH = 2 + """Intended for much longer inputs.""" + + +@attr.define(kw_only=True, weakref_slot=False) +class PartialComponent: + """Base class for all component entities.""" + + type: typing.Union[ComponentType, int] = attr.field() + """The type of component this is.""" + + +@attr.define(weakref_slot=False) +class ActionRowComponent(PartialComponent): + """Represents a row of components.""" + + components: typing.Sequence[PartialComponent] = attr.field() + """Sequence of the components contained within this row.""" + + @typing.overload + def __getitem__(self, index: int, /) -> PartialComponent: + ... + + @typing.overload + def __getitem__(self, slice_: slice, /) -> typing.Sequence[PartialComponent]: + ... + + def __getitem__( + self, index_or_slice: typing.Union[int, slice], / + ) -> typing.Union[PartialComponent, typing.Sequence[PartialComponent]]: + return self.components[index_or_slice] + + def __iter__(self) -> typing.Iterator[PartialComponent]: + return iter(self.components) + + def __len__(self) -> int: + return len(self.components) + + +@attr.define(hash=True, kw_only=True, weakref_slot=False) +class ButtonComponent(PartialComponent): + """Represents a button component.""" + + style: typing.Union[ButtonStyle, int] = attr.field(eq=False) + """The button's style.""" + + label: typing.Optional[str] = attr.field(eq=False) + """Text label which appears on the button.""" + + emoji: typing.Optional[emojis.Emoji] = attr.field(eq=False) + """Custom or unicode emoji which appears on the button.""" + + custom_id: typing.Optional[str] = attr.field(hash=True) + """Developer defined identifier for this button (will be <= 100 characters). + + !!! note + This is required for the following button styles: + + * `ButtonStyle.PRIMARY` + * `ButtonStyle.SECONDARY` + * `ButtonStyle.SUCCESS` + * `ButtonStyle.DANGER` + """ + + url: typing.Optional[str] = attr.field(eq=False) + """Url for `ButtonStyle.LINK` style buttons.""" + + is_disabled: bool = attr.field(eq=False) + """Whether the button is disabled.""" + + +@attr.define(kw_only=True, weakref_slot=False) +class SelectMenuOption: + """Represents an option for a `SelectMenuComponent`.""" + + label: str = attr.field() + """User-facing name of the option, max 100 characters.""" + + value: str = attr.field() + """Dev-defined value of the option, max 100 characters.""" + + description: typing.Optional[str] = attr.field() + """Optional description of the option, max 100 characters.""" + + emoji: typing.Optional[emojis.Emoji] = attr.field(eq=False) + """Custom or unicode emoji which appears on the button.""" + + is_default: bool = attr.field() + """Whether this option will be selected by default.""" + + +@attr.define(hash=True, kw_only=True, weakref_slot=False) +class SelectMenuComponent(PartialComponent): + """Represents a select menu component.""" + + 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.""" + + min_values: int = attr.field(eq=False) + """The minimum amount of options which must be chosen for this menu. + + This will be greater than or equal to 0 and will be less than or equal to + `SelectMenuComponent.max_values`. + """ + + max_values: int = attr.field(eq=False) + """The minimum amount of options which can be chosen for this menu. + + This will be less than or equal to 25 and will be greater than or equal to + `SelectMenuComponent.min_values`. + """ + + is_disabled: bool = attr.field(eq=False) + """Whether the select menu is disabled.""" + + +@attr.define(kw_only=True, weakref_slot=False) +class TextInputComponent(PartialComponent): + """Represents a text input component.""" + + custom_id: str = attr.field(repr=True) + """Developer set custom ID used for identifying interactions with this modal.""" + + value: str = attr.field(repr=True) + """Value provided for this text input.""" + + +InteractiveButtonTypesT = typing.Union[ + typing.Literal[ButtonStyle.PRIMARY], + typing.Literal[1], + typing.Literal[ButtonStyle.SECONDARY], + typing.Literal[2], + typing.Literal[ButtonStyle.SUCCESS], + typing.Literal[3], + typing.Literal[ButtonStyle.DANGER], + typing.Literal[4], +] +"""Type hints of the `ButtonStyle` values which are valid for interactive buttons. + +The following values are valid for this: + +* `ButtonStyle.PRIMARY`/`1` +* `ButtonStyle.SECONDARY`/`2` +* `ButtonStyle.SUCCESS`/`3` +* `ButtonStyle.DANGER`/`4` +""" + +InteractiveButtonTypes: typing.AbstractSet[InteractiveButtonTypesT] = frozenset( + [ButtonStyle.PRIMARY, ButtonStyle.SECONDARY, ButtonStyle.SUCCESS, ButtonStyle.DANGER] +) +"""Set of the `ButtonType`s which are valid for interactive buttons. + +The following values are included in this: + +* `ButtonStyle.PRIMARY` +* `ButtonStyle.SECONDARY` +* `ButtonStyle.SUCCESS` +* `ButtonStyle.DANGER` +""" diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index f448baa6cc..22f1ed24d9 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -37,6 +37,7 @@ from hikari import channels as channel_models from hikari import colors as color_models from hikari import commands +from hikari import components as component_models from hikari import embeds as embed_models from hikari import emojis as emoji_models from hikari import errors @@ -505,13 +506,10 @@ def __init__(self, app: traits.RESTAware) -> None: commands.CommandType.MESSAGE: self.deserialize_context_menu_command, } self._component_type_mapping = { - message_models.ComponentType.ACTION_ROW: self._deserialize_action_row, - message_models.ComponentType.BUTTON: self._deserialize_button, - message_models.ComponentType.SELECT_MENU: self._deserialize_select_menu, - } - self._modal_component_type_mapping = { - modal_interactions.ModalComponentType.ACTION_ROW: self._deserialize_modal_action_row, - modal_interactions.ModalComponentType.TEXT_INPUT: self._deserialize_text_input, + component_models.ComponentType.ACTION_ROW: self._deserialize_action_row, + component_models.ComponentType.BUTTON: self._deserialize_button, + component_models.ComponentType.SELECT_MENU: self._deserialize_select_menu, + component_models.ComponentType.TEXT_INPUT: self._deserialize_text_input, } self._dm_channel_type_mapping = { channel_models.ChannelType.DM: self.deserialize_dm, @@ -2385,7 +2383,7 @@ def deserialize_command_interaction( options=options, resolved=resolved, target_id=target_id, - app_permissions=permission_models.Permissions(app_perms) if app_perms is not None else None, + app_permissions=permission_models.Permissions(app_perms) if app_perms else None, ) def deserialize_autocomplete_interaction( @@ -2447,12 +2445,10 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod member = None user = self.deserialize_user(payload["user"]) - app_perms = payload.get("app_permissions") - - components: typing.List[modal_interactions.PartialModalComponent] = [] + components: typing.List[component_models.PartialComponent] = [] for component_payload in data_payload["components"]: try: - components.append(self._deserialize_modal_component(component_payload)) + components.append(self._deserialize_component(component_payload)) except errors.UnrecognisedEntityError: pass @@ -2460,13 +2456,14 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod if message_payload := payload.get("message"): message = self.deserialize_message(message_payload) + app_perms = payload.get("app_permissions") return modal_interactions.ModalInteraction( app=self._app, application_id=snowflakes.Snowflake(payload["application_id"]), id=snowflakes.Snowflake(payload["id"]), type=base_interactions.InteractionType(payload["type"]), guild_id=guild_id, - app_permissions=permission_models.Permissions(app_perms) if app_perms is not None else None, + app_permissions=permission_models.Permissions(app_perms) if app_perms else None, guild_locale=locales.Locale(payload["guild_locale"]) if "guild_locale" in payload else None, locale=locales.Locale(payload["locale"]), channel_id=snowflakes.Snowflake(payload["channel_id"]), @@ -2556,11 +2553,11 @@ def deserialize_component_interaction( values=data_payload.get("values") or (), version=payload["version"], custom_id=data_payload["custom_id"], - component_type=message_models.ComponentType(data_payload["component_type"]), + component_type=component_models.ComponentType(data_payload["component_type"]), message=self.deserialize_message(payload["message"]), locale=locales.Locale(payload["locale"]), guild_locale=locales.Locale(payload["guild_locale"]) if "guild_locale" in payload else None, - app_permissions=permission_models.Permissions(app_perms) if app_perms is not None else None, + app_permissions=permission_models.Permissions(app_perms) if app_perms else None, ) ################## @@ -2616,8 +2613,17 @@ def deserialize_guild_sticker(self, payload: data_binding.JSONObject) -> sticker # MESSAGE MODELS # ################## - def _deserialize_action_row(self, payload: data_binding.JSONObject) -> message_models.ActionRowComponent: - components: typing.List[message_models.PartialComponent] = [] + def _deserialize_component(self, payload: data_binding.JSONObject) -> component_models.PartialComponent: + component_type = component_models.ComponentType(payload["type"]) + + if deserialize := self._component_type_mapping.get(component_type): + return deserialize(payload) + + _LOGGER.debug("Unknown component type %s", component_type) + raise errors.UnrecognisedEntityError(f"Unrecognised component type {component_type}") + + def _deserialize_action_row(self, payload: data_binding.JSONObject) -> component_models.ActionRowComponent: + components: typing.List[component_models.PartialComponent] = [] for component_payload in payload["components"]: try: @@ -2626,15 +2632,15 @@ def _deserialize_action_row(self, payload: data_binding.JSONObject) -> message_m except errors.UnrecognisedEntityError: pass - return message_models.ActionRowComponent( - type=message_models.ComponentType(payload["type"]), components=components + return component_models.ActionRowComponent( + type=component_models.ComponentType(payload["type"]), components=components ) - def _deserialize_button(self, payload: data_binding.JSONObject) -> message_models.ButtonComponent: + def _deserialize_button(self, payload: data_binding.JSONObject) -> component_models.ButtonComponent: emoji_payload = payload.get("emoji") - return message_models.ButtonComponent( - type=message_models.ComponentType(payload["type"]), - style=message_models.ButtonStyle(payload["style"]), + return component_models.ButtonComponent( + type=component_models.ComponentType(payload["type"]), + style=component_models.ButtonStyle(payload["style"]), label=payload.get("label"), emoji=self.deserialize_emoji(emoji_payload) if emoji_payload else None, custom_id=payload.get("custom_id"), @@ -2642,15 +2648,15 @@ def _deserialize_button(self, payload: data_binding.JSONObject) -> message_model is_disabled=payload.get("disabled", False), ) - def _deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_models.SelectMenuComponent: - options: typing.List[message_models.SelectMenuOption] = [] + def _deserialize_select_menu(self, payload: data_binding.JSONObject) -> component_models.SelectMenuComponent: + 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( - message_models.SelectMenuOption( + component_models.SelectMenuOption( label=option_payload["label"], value=option_payload["value"], description=option_payload.get("description"), @@ -2659,8 +2665,8 @@ def _deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_ ) ) - return message_models.SelectMenuComponent( - type=message_models.ComponentType(payload["type"]), + return component_models.SelectMenuComponent( + type=component_models.ComponentType(payload["type"]), custom_id=payload["custom_id"], options=options, placeholder=payload.get("placeholder"), @@ -2669,49 +2675,13 @@ def _deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_ is_disabled=payload.get("disabled", False), ) - def _deserialize_component(self, payload: data_binding.JSONObject) -> message_models.PartialComponent: - component_type = message_models.ComponentType(payload["type"]) - - if deserialize := self._component_type_mapping.get(component_type): - return deserialize(payload) - - _LOGGER.debug("Unknown component type %s", component_type) - raise errors.UnrecognisedEntityError(f"Unrecognised component type {component_type}") - - def _deserialize_modal_action_row( - self, payload: data_binding.JSONObject - ) -> modal_interactions.ModalActionRowComponent: - components: typing.List[modal_interactions.PartialModalComponent] = [] - - for component_payload in payload["components"]: - try: - components.append(self._deserialize_modal_component(component_payload)) - - except errors.UnrecognisedEntityError: - pass - - return modal_interactions.ModalActionRowComponent( - type=message_models.ComponentType(payload["type"]), components=components - ) - - def _deserialize_text_input(self, payload: data_binding.JSONObject) -> modal_interactions.InteractionTextInput: - return modal_interactions.InteractionTextInput( - type=message_models.ComponentType(payload["type"]), + def _deserialize_text_input(self, payload: data_binding.JSONObject) -> component_models.TextInputComponent: + return component_models.TextInputComponent( + type=component_models.ComponentType(payload["type"]), custom_id=payload["custom_id"], value=payload["value"], ) - def _deserialize_modal_component( - self, payload: data_binding.JSONObject - ) -> modal_interactions.PartialModalComponent: - component_type = modal_interactions.ModalComponentType(payload["type"]) - - if deserialize := self._modal_component_type_mapping.get(component_type): - return deserialize(payload) - - _LOGGER.debug("Unknown component type %s", component_type) - raise errors.UnrecognisedEntityError(f"Unrecognised component type {component_type}") - def _deserialize_message_activity(self, payload: data_binding.JSONObject) -> message_models.MessageActivity: return message_models.MessageActivity( type=message_models.MessageActivityType(payload["type"]), party_id=payload.get("party_id") @@ -2848,7 +2818,7 @@ def deserialize_partial_message( # noqa CFQ001 - Function too long if interaction_payload := payload.get("interaction"): interaction = self._deserialize_message_interaction(interaction_payload) - components: undefined.UndefinedOr[typing.List[message_models.PartialComponent]] = undefined.UNDEFINED + components: undefined.UndefinedOr[typing.List[component_models.PartialComponent]] = undefined.UNDEFINED if component_payloads := payload.get("components"): components = [] for component_payload in component_payloads: @@ -2964,7 +2934,7 @@ def deserialize_message( if interaction_payload := payload.get("interaction"): interaction = self._deserialize_message_interaction(interaction_payload) - components: typing.List[message_models.PartialComponent] = [] + components: typing.List[component_models.PartialComponent] = [] if component_payloads := payload.get("components"): for component_payload in component_payloads: try: diff --git a/hikari/impl/interaction_server.py b/hikari/impl/interaction_server.py index 552a4909bb..f8e636fb79 100644 --- a/hikari/impl/interaction_server.py +++ b/hikari/impl/interaction_server.py @@ -63,7 +63,7 @@ special_endpoints.InteractionDeferredBuilder, special_endpoints.InteractionMessageBuilder, ] - _ModalResponseBuilderT = typing.Union[ + _ModalOrMessageResponseBuilderT = typing.Union[ _MessageResponseBuilderT, special_endpoints.InteractionModalBuilder, ] @@ -567,14 +567,16 @@ async def start( @typing.overload def get_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], / - ) -> typing.Optional[interaction_server.ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilderT]]: + ) -> typing.Optional[ + interaction_server.ListenerT[command_interactions.CommandInteraction, _ModalOrMessageResponseBuilderT] + ]: ... @typing.overload def get_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], / ) -> typing.Optional[ - interaction_server.ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilderT] + interaction_server.ListenerT[component_interactions.ComponentInteraction, _ModalOrMessageResponseBuilderT] ]: ... @@ -610,7 +612,7 @@ def set_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], listener: typing.Optional[ - interaction_server.ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilderT] + interaction_server.ListenerT[command_interactions.CommandInteraction, _ModalOrMessageResponseBuilderT] ], /, *, @@ -623,7 +625,7 @@ def set_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], listener: typing.Optional[ - interaction_server.ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilderT] + interaction_server.ListenerT[component_interactions.ComponentInteraction, _ModalOrMessageResponseBuilderT] ], /, *, diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 3f99fc8415..2334103516 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -3960,23 +3960,8 @@ def interaction_message_builder( ) -> special_endpoints.InteractionMessageBuilder: return special_endpoints_impl.InteractionMessageBuilder(type=type_) - def interaction_modal_builder( - self, - title: str, - custom_id: str, - *, - components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, - ) -> special_endpoints.InteractionModalBuilder: - if components is undefined.UNDEFINED: - components = [] - else: - components = list(components) - - return special_endpoints_impl.InteractionModalBuilder( - title=title, - custom_id=custom_id, - components=components, - ) + def interaction_modal_builder(self, title: str, custom_id: str) -> special_endpoints.InteractionModalBuilder: + return special_endpoints_impl.InteractionModalBuilder(title=title, custom_id=custom_id) async def fetch_interaction_response( self, application: snowflakes.SnowflakeishOr[guilds.PartialApplication], token: str @@ -4118,8 +4103,12 @@ async def create_modal_response( *, title: str, custom_id: str, - components: typing.Sequence[special_endpoints.ComponentBuilder], + component: undefined.UndefinedOr[special_endpoints.ComponentBuilder] = undefined.UNDEFINED, + components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, ) -> None: + if undefined.all_undefined(component, components) or not undefined.any_undefined(component, components): + raise ValueError("Must specify exactly only one of component or components") + route = routes.POST_INTERACTION_RESPONSE.compile(interaction=interaction, token=token) body = data_binding.JSONObjectBuilder() @@ -4128,14 +4117,33 @@ async def create_modal_response( data = data_binding.JSONObjectBuilder() data.put("title", title) data.put("custom_id", custom_id) - data.put_array("components", components, conversion=lambda component: component.build()) + + if component: + components = (component,) + + data.put_array("components", components, conversion=lambda c: c.build()) body.put("data", data) await self._request(route, json=body, no_auth=True) - def build_action_row(self) -> special_endpoints.ActionRowBuilder: - return special_endpoints_impl.ActionRowBuilder() + def build_action_row(self) -> special_endpoints.MessageActionRowBuilder: + """Build a message action row message component for use in message create and REST calls. + + Returns + ------- + hikari.api.special_endpoints.MessageActionRowBuilder + The initialised action row builder. + """ + deprecation.warn_deprecated( + "build_action_row", + removal_version="2.0.0.dev115", + additional_info="Use 'build_message_action_row' parameter instead", + ) + return special_endpoints_impl.MessageActionRowBuilder() + + def build_message_action_row(self) -> special_endpoints.MessageActionRowBuilder: + return special_endpoints_impl.MessageActionRowBuilder() def build_modal_action_row(self) -> special_endpoints.ModalActionRowBuilder: return special_endpoints_impl.ModalActionRowBuilder() diff --git a/hikari/impl/rest_bot.py b/hikari/impl/rest_bot.py index 5bbf1202b9..79c8e6566e 100644 --- a/hikari/impl/rest_bot.py +++ b/hikari/impl/rest_bot.py @@ -60,7 +60,7 @@ special_endpoints.InteractionDeferredBuilder, special_endpoints.InteractionMessageBuilder, ] - _ModalResponseBuilderT = typing.Union[ + _ModalOrMessageResponseBuilderT = typing.Union[ _MessageResponseBuilderT, special_endpoints.InteractionModalBuilder, ] @@ -653,7 +653,7 @@ async def start( def get_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], / ) -> typing.Optional[ - interaction_server_.ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilderT] + interaction_server_.ListenerT[command_interactions.CommandInteraction, _ModalOrMessageResponseBuilderT] ]: ... @@ -661,7 +661,7 @@ def get_listener( def get_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], / ) -> typing.Optional[ - interaction_server_.ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilderT] + interaction_server_.ListenerT[component_interactions.ComponentInteraction, _ModalOrMessageResponseBuilderT] ]: ... @@ -697,7 +697,7 @@ def set_listener( self, interaction_type: typing.Type[command_interactions.CommandInteraction], listener: typing.Optional[ - interaction_server_.ListenerT[command_interactions.CommandInteraction, _ModalResponseBuilderT] + interaction_server_.ListenerT[command_interactions.CommandInteraction, _ModalOrMessageResponseBuilderT] ], /, *, @@ -710,7 +710,7 @@ def set_listener( self, interaction_type: typing.Type[component_interactions.ComponentInteraction], listener: typing.Optional[ - interaction_server_.ListenerT[component_interactions.ComponentInteraction, _ModalResponseBuilderT] + interaction_server_.ListenerT[component_interactions.ComponentInteraction, _ModalOrMessageResponseBuilderT] ], /, *, diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 6197e4ae9d..df39c99583 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -27,7 +27,6 @@ from __future__ import annotations __all__: typing.Sequence[str] = ( - "ActionRowBuilder", "CommandBuilder", "SlashCommandBuilder", "ContextMenuCommandBuilder", @@ -41,6 +40,7 @@ "SelectMenuBuilder", "TextInputBuilder", "InteractionModalBuilder", + "MessageActionRowBuilder", "ModalActionRowBuilder", ) @@ -51,6 +51,7 @@ from hikari import channels from hikari import commands +from hikari import components as component_models from hikari import emojis from hikari import errors from hikari import files @@ -61,7 +62,6 @@ from hikari import undefined from hikari.api import special_endpoints from hikari.interactions import base_interactions -from hikari.interactions import modal_interactions from hikari.internal import attr_extensions from hikari.internal import data_binding from hikari.internal import mentions @@ -93,7 +93,7 @@ "_InteractionAutocompleteBuilderT", bound="InteractionAutocompleteBuilder" ) _InteractionModalBuilderT = typing.TypeVar("_InteractionModalBuilderT", bound="InteractionModalBuilder") - _ActionRowBuilderT = typing.TypeVar("_ActionRowBuilderT", bound="ActionRowBuilder") + _MessageActionRowBuilderT = typing.TypeVar("_MessageActionRowBuilderT", bound="MessageActionRowBuilder") _ModalActionRowBuilderT = typing.TypeVar("_ModalActionRowBuilderT", bound="ModalActionRowBuilder") _ButtonBuilderT = typing.TypeVar("_ButtonBuilderT", bound="_ButtonBuilder[typing.Any]") _SelectOptionBuilderT = typing.TypeVar("_SelectOptionBuilderT", bound="_SelectOptionBuilder[typing.Any]") @@ -1449,7 +1449,7 @@ def _build_emoji( @attr.define(kw_only=True, weakref_slot=False) class _ButtonBuilder(special_endpoints.ButtonBuilder[_ContainerProtoT]): _container: _ContainerProtoT = attr.field() - _style: typing.Union[int, messages.ButtonStyle] = attr.field() + _style: typing.Union[int, component_models.ButtonStyle] = attr.field() _custom_id: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED) _url: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED) _emoji: typing.Union[snowflakes.Snowflakeish, emojis.Emoji, str, undefined.UndefinedType] = attr.field( @@ -1461,7 +1461,7 @@ class _ButtonBuilder(special_endpoints.ButtonBuilder[_ContainerProtoT]): _is_disabled: bool = attr.field(default=False) @property - def style(self) -> typing.Union[int, messages.ButtonStyle]: + def style(self) -> typing.Union[int, component_models.ButtonStyle]: return self._style @property @@ -1500,7 +1500,7 @@ def add_to_container(self) -> _ContainerProtoT: def build(self) -> typing.MutableMapping[str, typing.Any]: data = data_binding.JSONObjectBuilder() - data["type"] = messages.ComponentType.BUTTON + data["type"] = component_models.ComponentType.BUTTON data["style"] = self._style data["disabled"] = self._is_disabled data.put("label", self._label) @@ -1689,7 +1689,7 @@ def add_to_container(self) -> _ContainerProtoT: def build(self) -> typing.MutableMapping[str, typing.Any]: data = data_binding.JSONObjectBuilder() - data["type"] = messages.ComponentType.SELECT_MENU + data["type"] = component_models.ComponentType.SELECT_MENU data["custom_id"] = self._custom_id data["options"] = [option.build() for option in self._options] data.put("placeholder", self._placeholder) @@ -1708,7 +1708,7 @@ class TextInputBuilder(special_endpoints.TextInputBuilder[_ContainerProtoT]): _custom_id: str = attr.field() _label: str = attr.field() - _style: modal_interactions.TextInputStyle = attr.field(default=modal_interactions.TextInputStyle.SHORT) + _style: component_models.TextInputStyle = attr.field(default=component_models.TextInputStyle.SHORT) _placeholder: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED, kw_only=True) _value: undefined.UndefinedOr[str] = attr.field(default=undefined.UNDEFINED, kw_only=True) _required: undefined.UndefinedOr[bool] = attr.field(default=undefined.UNDEFINED, kw_only=True) @@ -1724,7 +1724,7 @@ def label(self) -> str: return self._label @property - def style(self) -> modal_interactions.TextInputStyle: + def style(self) -> component_models.TextInputStyle: return self._style @property @@ -1748,9 +1748,9 @@ def max_length(self) -> undefined.UndefinedOr[int]: return self._max_length def set_style( - self: _TextInputBuilderT, style: typing.Union[modal_interactions.TextInputStyle, int], / + self: _TextInputBuilderT, style: typing.Union[component_models.TextInputStyle, int], / ) -> _TextInputBuilderT: - self._style = modal_interactions.TextInputStyle(style) + self._style = component_models.TextInputStyle(style) return self def set_custom_id(self: _TextInputBuilderT, custom_id: str, /) -> _TextInputBuilderT: @@ -1788,7 +1788,7 @@ def add_to_container(self) -> _ContainerProtoT: def build(self) -> typing.MutableMapping[str, typing.Any]: data = data_binding.JSONObjectBuilder() - data["type"] = modal_interactions.ModalComponentType.TEXT_INPUT + data["type"] = component_models.ComponentType.TEXT_INPUT data["style"] = self._style data["custom_id"] = self._custom_id data["label"] = self._label @@ -1802,17 +1802,17 @@ def build(self) -> typing.MutableMapping[str, typing.Any]: @attr.define(kw_only=True, weakref_slot=False) -class ActionRowBuilder(special_endpoints.ActionRowBuilder): +class MessageActionRowBuilder(special_endpoints.MessageActionRowBuilder): """Standard implementation of `hikari.api.special_endpoints.ActionRowBuilder`.""" _components: typing.List[special_endpoints.ComponentBuilder] = attr.field(factory=list) - _stored_type: typing.Optional[messages.ComponentType] = attr.field(default=None) + _stored_type: typing.Optional[component_models.ComponentType] = attr.field(default=None) @property def components(self) -> typing.Sequence[special_endpoints.ComponentBuilder]: return self._components.copy() - def _assert_can_add_type(self, type_: messages.ComponentType, /) -> None: + def _assert_can_add_type(self, type_: component_models.ComponentType, /) -> None: if self._stored_type is not None and self._stored_type != type_: raise ValueError( f"{type_} component type cannot be added to a container which already holds {self._stored_type}" @@ -1820,55 +1820,63 @@ def _assert_can_add_type(self, type_: messages.ComponentType, /) -> None: self._stored_type = type_ - def add_component(self: _ActionRowBuilderT, component: special_endpoints.ComponentBuilder, /) -> _ActionRowBuilderT: + def add_component( + self: _MessageActionRowBuilderT, component: special_endpoints.ComponentBuilder, / + ) -> _MessageActionRowBuilderT: self._components.append(component) return self @typing.overload def add_button( - self: _ActionRowBuilderT, style: messages.InteractiveButtonTypesT, custom_id: str, / - ) -> special_endpoints.InteractiveButtonBuilder[_ActionRowBuilderT]: + self: _MessageActionRowBuilderT, style: component_models.InteractiveButtonTypesT, custom_id: str, / + ) -> special_endpoints.InteractiveButtonBuilder[_MessageActionRowBuilderT]: ... @typing.overload def add_button( - self: _ActionRowBuilderT, - style: typing.Literal[messages.ButtonStyle.LINK, 5], + self: _MessageActionRowBuilderT, + style: typing.Literal[component_models.ButtonStyle.LINK, 5], url: str, /, - ) -> special_endpoints.LinkButtonBuilder[_ActionRowBuilderT]: + ) -> special_endpoints.LinkButtonBuilder[_MessageActionRowBuilderT]: ... @typing.overload def add_button( - self: _ActionRowBuilderT, style: typing.Union[int, messages.ButtonStyle], url_or_custom_id: str, / + self: _MessageActionRowBuilderT, + style: typing.Union[int, component_models.ButtonStyle], + url_or_custom_id: str, + /, ) -> typing.Union[ - special_endpoints.LinkButtonBuilder[_ActionRowBuilderT], - special_endpoints.InteractiveButtonBuilder[_ActionRowBuilderT], + special_endpoints.LinkButtonBuilder[_MessageActionRowBuilderT], + special_endpoints.InteractiveButtonBuilder[_MessageActionRowBuilderT], ]: ... def add_button( - self: _ActionRowBuilderT, style: typing.Union[int, messages.ButtonStyle], url_or_custom_id: str, / + self: _MessageActionRowBuilderT, + style: typing.Union[int, component_models.ButtonStyle], + url_or_custom_id: str, + /, ) -> typing.Union[ - special_endpoints.LinkButtonBuilder[_ActionRowBuilderT], - special_endpoints.InteractiveButtonBuilder[_ActionRowBuilderT], + special_endpoints.LinkButtonBuilder[_MessageActionRowBuilderT], + special_endpoints.InteractiveButtonBuilder[_MessageActionRowBuilderT], ]: - self._assert_can_add_type(messages.ComponentType.BUTTON) - if style in messages.InteractiveButtonTypes: + self._assert_can_add_type(component_models.ComponentType.BUTTON) + if style in component_models.InteractiveButtonTypes: return InteractiveButtonBuilder(container=self, style=style, custom_id=url_or_custom_id) return LinkButtonBuilder(container=self, style=style, url=url_or_custom_id) def add_select_menu( - self: _ActionRowBuilderT, custom_id: str, / - ) -> special_endpoints.SelectMenuBuilder[_ActionRowBuilderT]: - self._assert_can_add_type(messages.ComponentType.SELECT_MENU) + self: _MessageActionRowBuilderT, 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) def build(self) -> typing.MutableMapping[str, typing.Any]: return { - "type": messages.ComponentType.ACTION_ROW, + "type": component_models.ComponentType.ACTION_ROW, "components": [component.build() for component in self._components], } @@ -1878,13 +1886,13 @@ class ModalActionRowBuilder(special_endpoints.ModalActionRowBuilder): """Standard implementation of `hikari.api.special_endpoints.ActionRowBuilder`.""" _components: typing.List[special_endpoints.ComponentBuilder] = attr.field(factory=list) - _stored_type: typing.Optional[modal_interactions.ModalComponentType] = attr.field(default=None) + _stored_type: typing.Optional[component_models.ComponentType] = attr.field(default=None) @property def components(self) -> typing.Sequence[special_endpoints.ComponentBuilder]: return self._components.copy() - def _assert_can_add_type(self, type_: modal_interactions.ModalComponentType, /) -> None: + def _assert_can_add_type(self, type_: component_models.ComponentType, /) -> None: if self._stored_type is not None and self._stored_type != type_: raise ValueError( f"{type_} component type cannot be added to a container which already holds {self._stored_type}" @@ -1903,11 +1911,11 @@ def add_text_input( custom_id: str, label: str, ) -> special_endpoints.TextInputBuilder[_ModalActionRowBuilderT]: - self._assert_can_add_type(modal_interactions.ModalComponentType.TEXT_INPUT) + self._assert_can_add_type(component_models.ComponentType.TEXT_INPUT) return TextInputBuilder(container=self, custom_id=custom_id, label=label) def build(self) -> typing.MutableMapping[str, typing.Any]: return { - "type": messages.ComponentType.ACTION_ROW, + "type": component_models.ComponentType.ACTION_ROW, "components": [component.build() for component in self._components], } diff --git a/hikari/interactions/base_interactions.py b/hikari/interactions/base_interactions.py index 9ac65caeae..5fa2c37e6e 100644 --- a/hikari/interactions/base_interactions.py +++ b/hikari/interactions/base_interactions.py @@ -584,7 +584,8 @@ async def create_modal_response( self, title: str, custom_id: str, - components: typing.Sequence[special_endpoints.ComponentBuilder], + component: undefined.UndefinedOr[special_endpoints.ComponentBuilder] = undefined.UNDEFINED, + components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, ) -> None: """Create a response by sending a modal. @@ -594,24 +595,29 @@ async def create_modal_response( The title that will show up in the modal. custom_id : str Developer set custom ID used for identifying interactions with this modal. - components : typing.Sequence[special_endpoints.ComponentBuilder] - The components to display in the modal. + + Other Parameters + ---------------- + component : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] + A component builders to send in this modal. + components : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] + A sequence of component builders to send in this modal. + + Raises + ------ + ValueError + If both `component` and `components` are specified or if none are specified. """ await self.app.rest.create_modal_response( self.id, self.token, title=title, custom_id=custom_id, + component=component, components=components, ) - def build_modal_response( - self, - title: str, - custom_id: str, - *, - components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, - ) -> special_endpoints.InteractionModalBuilder: + def build_modal_response(self, title: str, custom_id: str) -> special_endpoints.InteractionModalBuilder: """Create a builder for a modal interaction response. Parameters @@ -620,15 +626,13 @@ def build_modal_response( The title that will show up in the modal. custom_id : builtins.str Developer set custom ID used for identifying interactions with this modal. - components : hikari.undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] - Sequence of component builders to send in this modal. Returns ------- hikari.api.special_endpoints.InteractionModalBuilder The interaction modal response builder object. """ - return self.app.rest.interaction_modal_builder(title=title, custom_id=custom_id, components=components) + return self.app.rest.interaction_modal_builder(title=title, custom_id=custom_id) @attr.define(hash=True, kw_only=True, weakref_slot=False) diff --git a/hikari/interactions/component_interactions.py b/hikari/interactions/component_interactions.py index 6f1bd2d269..6c7b77e828 100644 --- a/hikari/interactions/component_interactions.py +++ b/hikari/interactions/component_interactions.py @@ -35,6 +35,7 @@ from hikari.internal import deprecation if typing.TYPE_CHECKING: + from hikari import components as components_ from hikari import guilds from hikari import locales from hikari import messages @@ -93,7 +94,7 @@ class ComponentInteraction( channel_id: snowflakes.Snowflake = attr.field(eq=False) """ID of the channel this interaction was triggered in.""" - component_type: typing.Union[messages.ComponentType, int] = attr.field(eq=False) + component_type: typing.Union[components_.ComponentType, int] = attr.field(eq=False) """The type of component which triggers this interaction. !!! note diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index 0a1bf035d5..e062e9614f 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -27,12 +27,7 @@ __all__: typing.List[str] = [ "ModalResponseTypesT", "ModalInteraction", - "InteractionTextInput", "ModalInteraction", - "TextInputStyle", - "ModalComponentType", - "PartialModalComponent", - "ModalActionRowComponent", ] import typing @@ -47,14 +42,21 @@ from hikari import traits from hikari.interactions import base_interactions from hikari.internal import attr_extensions -from hikari.internal import enums if typing.TYPE_CHECKING: + from hikari import components as components_ from hikari import users as _users from hikari.api import special_endpoints ModalResponseTypesT = typing.Literal[ - base_interactions.ResponseType.MESSAGE_CREATE, 4, base_interactions.ResponseType.DEFERRED_MESSAGE_CREATE, 5 + base_interactions.ResponseType.MESSAGE_CREATE, + 4, + base_interactions.ResponseType.DEFERRED_MESSAGE_CREATE, + 5, + base_interactions.ResponseType.MESSAGE_UPDATE, + 7, + base_interactions.ResponseType.DEFERRED_MESSAGE_UPDATE, + 6, ] """Type-hint of the response types which are valid for a modal interaction. @@ -62,93 +64,11 @@ * `hikari.interactions.base_interactions.ResponseType.MESSAGE_CREATE`/`4` * `hikari.interactions.base_interactions.ResponseType.DEFERRED_MESSAGE_CREATE`/`5` +* `hikari.interactions.base_interactions.ResponseType.MESSAGE_UPDATE`/`7` +* `hikari.interactions.base_interactions.ResponseType.DEFERRED_MESSAGE_UPDATE`/`6` """ -@typing.final -class ModalComponentType(int, enums.Enum): - """Types of components found within Discord.""" - - ACTION_ROW = 1 - """A non-interactive container component for other types of components. - - !!! note - As this is a container component it can never be contained within another - component and therefore will always be top-level. - - !!! note - As of writing this can only contain one component type. - """ - - TEXT_INPUT = 4 - """A text input component - - !! note - This component may only be used inside a modal container. - """ - - -class TextInputStyle(int, enums.Enum): - """A text input style.""" - - SHORT = 1 - """Intended for short single-line text.""" - - PARAGRAPH = 2 - """Intended for much longer inputs.""" - - -@attr.define(kw_only=True, weakref_slot=False) -class PartialModalComponent: - """Base class for all model component entities.""" - - type: typing.Union[ModalComponentType, int] = attr.field() - """The type of component this is.""" - - -@attr.define(weakref_slot=False) -class ModalActionRowComponent(PartialModalComponent): - """Represents a row of components attached to a message. - - !!! note - This is a top-level container component and will never be found within - another component. - """ - - components: typing.Sequence[PartialModalComponent] = attr.field() - """Sequence of the components contained within this row.""" - - @typing.overload - def __getitem__(self, index: int, /) -> PartialModalComponent: - ... - - @typing.overload - def __getitem__(self, slice_: slice, /) -> typing.Sequence[PartialModalComponent]: - ... - - def __getitem__( - self, index_or_slice: typing.Union[int, slice], / - ) -> typing.Union[PartialModalComponent, typing.Sequence[PartialModalComponent]]: - return self.components[index_or_slice] - - def __iter__(self) -> typing.Iterator[PartialModalComponent]: - return iter(self.components) - - def __len__(self) -> int: - return len(self.components) - - -@attr.define(kw_only=True, weakref_slot=False) -class InteractionTextInput(PartialModalComponent): - """A text input component in a modal interaction.""" - - custom_id: str = attr.field(repr=True) - """Developer set custom ID used for identifying interactions with this modal.""" - - value: str = attr.field(repr=True) - """Value provided for this text input.""" - - @attr_extensions.with_copy @attr.define(hash=True, kw_only=True, weakref_slot=False) class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypesT]): @@ -201,7 +121,7 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes app_permissions: typing.Optional[permissions.Permissions] = attr.field(eq=False, hash=False, repr=False) """Permissions the bot has in this interaction's channel if it's in a guild.""" - components: typing.Sequence[PartialModalComponent] = attr.field(eq=False, hash=False, repr=True) + components: typing.Sequence[components_.PartialComponent] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" async def fetch_channel(self) -> channels.TextableChannel: @@ -257,7 +177,7 @@ def get_channel(self) -> typing.Optional[channels.TextableGuildChannel]: """ if isinstance(self.app, traits.CacheAware): channel = self.app.cache.get_guild_channel(self.channel_id) - assert isinstance(channel, channels.TextableGuildChannel) + assert channel is None or isinstance(channel, channels.TextableGuildChannel) return channel return None diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py index 7453d2627b..f543ea07ee 100644 --- a/hikari/internal/cache.py +++ b/hikari/internal/cache.py @@ -71,6 +71,7 @@ if typing.TYPE_CHECKING: from hikari import applications from hikari import channels as channels_ + from hikari import components as components_ from hikari import traits from hikari import users as users_ from hikari.interactions import base_interactions @@ -771,7 +772,7 @@ class MessageData(BaseData[messages.Message]): referenced_message: typing.Optional[RefCell[MessageData]] = attr.field() interaction: typing.Optional[MessageInteractionData] = attr.field() application_id: typing.Optional[snowflakes.Snowflake] = attr.field() - components: typing.Tuple[messages.PartialComponent, ...] = attr.field() + components: typing.Tuple[components_.PartialComponent, ...] = attr.field() @classmethod def build_from_entity( diff --git a/hikari/messages.py b/hikari/messages.py index 8ddd1f6fac..58cb28e7fe 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -36,21 +36,13 @@ "MessageReference", "PartialMessage", "Message", - "ActionRowComponent", - "ButtonComponent", - "ButtonStyle", - "SelectMenuOption", - "SelectMenuComponent", - "InteractiveButtonTypes", - "InteractiveButtonTypesT", - "ComponentType", - "PartialComponent", ) import typing import attr +from hikari import components as component_models from hikari import files from hikari import guilds from hikari import snowflakes @@ -506,234 +498,6 @@ class MessageInteraction: """Object of the user who invoked this interaction.""" -@typing.final -class ComponentType(int, enums.Enum): - """Types of components found within Discord.""" - - ACTION_ROW = 1 - """A non-interactive container component for other types of components. - - !!! note - As this is a container component it can never be contained within another - component and therefore will always be top-level. - - !!! note - As of writing this can only contain one component type. - """ - - BUTTON = 2 - """A button component. - - !!! note - This cannot be top-level and must be within a container component such - as `ComponentType.ACTION_ROW`. - """ - - SELECT_MENU = 3 - """A select menu 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): - """Enum of the available button styles. - - More information, such as how these look, can be found at - https://discord.com/developers/docs/interactions/message-components#buttons-button-styles - """ - - PRIMARY = 1 - """A blurple "call to action" button.""" - - SECONDARY = 2 - """A grey neutral button.""" - - SUCCESS = 3 - """A green button.""" - - DANGER = 4 - """A red button (usually indicates a destructive action).""" - - LINK = 5 - """A grey button which navigates to a URL. - - !!! warning - Unlike the other button styles, clicking this one will not trigger an - interaction and custom_id shouldn't be included for this style. - """ - - -InteractiveButtonTypesT = typing.Union[ - typing.Literal[ButtonStyle.PRIMARY], - typing.Literal[1], - typing.Literal[ButtonStyle.SECONDARY], - typing.Literal[2], - typing.Literal[ButtonStyle.SUCCESS], - typing.Literal[3], - typing.Literal[ButtonStyle.DANGER], - typing.Literal[4], -] -"""Type hints of the `ButtonStyle` values which are valid for interactive buttons. - -The following values are valid for this: - -* `ButtonStyle.PRIMARY`/`1` -* `ButtonStyle.SECONDARY`/`2` -* `ButtonStyle.SUCCESS`/`3` -* `ButtonStyle.DANGER`/`4` -""" - -InteractiveButtonTypes: typing.AbstractSet[InteractiveButtonTypesT] = frozenset( - [ButtonStyle.PRIMARY, ButtonStyle.SECONDARY, ButtonStyle.SUCCESS, ButtonStyle.DANGER] -) -"""Set of the `ButtonType`s which are valid for interactive buttons. - -The following values are included in this: - -* `ButtonStyle.PRIMARY` -* `ButtonStyle.SECONDARY` -* `ButtonStyle.SUCCESS` -* `ButtonStyle.DANGER` -""" - - -@attr.define(kw_only=True, weakref_slot=False) -class PartialComponent: - """Base class for all component entities.""" - - type: typing.Union[ComponentType, int] = attr.field() - """The type of component this is.""" - - -@attr.define(hash=True, kw_only=True, weakref_slot=False) -class ButtonComponent(PartialComponent): - """Represents a message button component. - - !!! note - This is an embedded component and will only ever be found within - top-level container components such as `ActionRowComponent`. - """ - - style: typing.Union[ButtonStyle, int] = attr.field(eq=False) - """The button's style.""" - - label: typing.Optional[str] = attr.field(eq=False) - """Text label which appears on the button.""" - - emoji: typing.Optional[emojis_.Emoji] = attr.field(eq=False) - """Custom or unicode emoji which appears on the button.""" - - custom_id: typing.Optional[str] = attr.field(hash=True) - """Developer defined identifier for this button (will be <= 100 characters). - - !!! note - This is required for the following button styles: - - * `ButtonStyle.PRIMARY` - * `ButtonStyle.SECONDARY` - * `ButtonStyle.SUCCESS` - * `ButtonStyle.DANGER` - """ - - url: typing.Optional[str] = attr.field(eq=False) - """Url for `ButtonStyle.LINK` style buttons.""" - - is_disabled: bool = attr.field(eq=False) - """Whether the button is disabled.""" - - -@attr.define(kw_only=True, weakref_slot=False) -class SelectMenuOption: - """Represents an option for a `SelectMenuComponent`.""" - - label: str = attr.field() - """User-facing name of the option, max 100 characters.""" - - value: str = attr.field() - """Dev-defined value of the option, max 100 characters.""" - - description: typing.Optional[str] = attr.field() - """Optional description of the option, max 100 characters.""" - - emoji: typing.Optional[emojis_.Emoji] = attr.field(eq=False) - """Custom or unicode emoji which appears on the button.""" - - is_default: bool = attr.field() - """Whether this option will be selected by default.""" - - -@attr.define(hash=True, kw_only=True, weakref_slot=False) -class SelectMenuComponent(PartialComponent): - """Represents a message button component. - - !!! note - This is an embedded component and will only ever be found within - top-level container components such as `ActionRowComponent`. - """ - - 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.""" - - min_values: int = attr.field(eq=False) - """The minimum amount of options which must be chosen for this menu. - - This will be greater than or equal to 0 and will be less than or equal to - `SelectMenuComponent.max_values`. - """ - - max_values: int = attr.field(eq=False) - """The minimum amount of options which can be chosen for this menu. - - This will be less than or equal to 25 and will be greater than or equal to - `SelectMenuComponent.min_values`. - """ - - is_disabled: bool = attr.field(eq=False) - """Whether the select menu is disabled.""" - - -@attr.define(weakref_slot=False) -class ActionRowComponent(PartialComponent): - """Represents a row of components attached to a message. - - !!! note - This is a top-level container component and will never be found within - another component. - """ - - components: typing.Sequence[PartialComponent] = attr.field() - """Sequence of the components contained within this row.""" - - @typing.overload - def __getitem__(self, index: int, /) -> PartialComponent: - ... - - @typing.overload - def __getitem__(self, slice_: slice, /) -> typing.Sequence[PartialComponent]: - ... - - def __getitem__( - self, index_or_slice: typing.Union[int, slice], / - ) -> typing.Union[PartialComponent, typing.Sequence[PartialComponent]]: - return self.components[index_or_slice] - - def __iter__(self) -> typing.Iterator[PartialComponent]: - return iter(self.components) - - def __len__(self) -> int: - return len(self.components) - - def _map_cache_maybe_discover( ids: typing.Iterable[snowflakes.Snowflake], cache_call: typing.Callable[[snowflakes.Snowflake], typing.Optional[_T]], @@ -944,7 +708,9 @@ class PartialMessage(snowflakes.Unique): This will only be provided for interaction messages. """ - components: undefined.UndefinedOr[typing.Sequence[PartialComponent]] = attr.field(hash=False, eq=False, repr=False) + components: undefined.UndefinedOr[typing.Sequence[component_models.PartialComponent]] = attr.field( + hash=False, eq=False, repr=False + ) """Sequence of the components attached to this message.""" @property @@ -1779,5 +1545,5 @@ class Message(PartialMessage): This will only be provided for interaction messages. """ - components: typing.Sequence[PartialComponent] = attr.field(hash=False, eq=False, repr=False) + components: typing.Sequence[component_models.PartialComponent] = attr.field(hash=False, eq=False, repr=False) """Sequence of the components attached to this message.""" diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index fb8eb53395..15edbd746a 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -30,6 +30,7 @@ from hikari import channels as channel_models from hikari import colors as color_models from hikari import commands +from hikari import components as component_models from hikari import embeds as embed_models from hikari import emojis as emoji_models from hikari import errors @@ -4520,7 +4521,7 @@ def test_deserialize_component_interaction( assert interaction.token == "unique_interaction_token" assert interaction.version == 1 assert interaction.channel_id == 345626669114982999 - assert interaction.component_type is message_models.ComponentType.BUTTON + assert interaction.component_type is component_models.ComponentType.BUTTON assert interaction.custom_id == "click_one" assert interaction.guild_id == 290926798626357999 assert interaction.message == entity_factory_impl.deserialize_message(message_payload) @@ -4564,7 +4565,6 @@ def test_deserialize_component_interaction_with_undefined_fields( @pytest.fixture() def modal_interaction_payload(self, interaction_member_payload, message_payload): - # taken from ddocs return { "version": 1, "type": 5, @@ -4611,11 +4611,11 @@ def test_deserialize_modal_interaction( assert isinstance(interaction, modal_interactions.ModalInteraction) short_action_row = interaction.components[0] - assert isinstance(short_action_row, modal_interactions.ModalActionRowComponent) + assert isinstance(short_action_row, component_models.ActionRowComponent) short_text_input = short_action_row.components[0] - assert isinstance(short_text_input, modal_interactions.InteractionTextInput) + assert isinstance(short_text_input, component_models.TextInputComponent) assert short_text_input.value == "Wumpus" - assert short_text_input.type == modal_interactions.ModalComponentType.TEXT_INPUT + assert short_text_input.type == component_models.ComponentType.TEXT_INPUT assert short_text_input.custom_id == "name" def test_deserialize_modal_interaction_with_user( @@ -5086,7 +5086,7 @@ def action_row_payload(self, button_payload): def test__deserialize_action_row(self, entity_factory_impl, action_row_payload, button_payload): action_row = entity_factory_impl._deserialize_action_row(action_row_payload) - assert action_row.type is message_models.ComponentType.ACTION_ROW + assert action_row.type is component_models.ComponentType.ACTION_ROW assert action_row.components == [entity_factory_impl._deserialize_component(button_payload)] def test__deserialize_action_row_handles_unknown_component_type(self, entity_factory_impl): @@ -5111,8 +5111,8 @@ def button_payload(self, custom_emoji_payload): def test_deserialize__deserialize_button(self, entity_factory_impl, button_payload, custom_emoji_payload): button = entity_factory_impl._deserialize_button(button_payload) - assert button.type is message_models.ComponentType.BUTTON - assert button.style is message_models.ButtonStyle.PRIMARY + assert button.type is component_models.ComponentType.BUTTON + assert button.style is component_models.ButtonStyle.PRIMARY assert button.label == "Click me!" assert button.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) assert button.custom_id == "click_one" @@ -5124,8 +5124,8 @@ def test_deserialize__deserialize_button_with_unset_fields( ): button = entity_factory_impl._deserialize_button({"type": 2, "style": 5}) - assert button.type is message_models.ComponentType.BUTTON - assert button.style is message_models.ButtonStyle.LINK + assert button.type is component_models.ComponentType.BUTTON + assert button.style is component_models.ButtonStyle.LINK assert button.label is None assert button.emoji is None assert button.custom_id is None @@ -5155,7 +5155,7 @@ def select_menu_payload(self, custom_emoji_payload): 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) - assert menu.type is message_models.ComponentType.SELECT_MENU + assert menu.type is component_models.ComponentType.SELECT_MENU assert menu.custom_id == "Not an ID" # SelectMenuOption @@ -5166,7 +5166,7 @@ def test__deserialize_select_menu(self, entity_factory_impl, select_menu_payload assert option.description == "queen" assert option.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) assert option.is_default is True - assert isinstance(option, message_models.SelectMenuOption) + assert isinstance(option, component_models.SelectMenuOption) assert menu.placeholder == "Imagine a place" assert menu.min_values == 69 diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 1d76a17867..6737fbdfad 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -1209,11 +1209,17 @@ def test_context_menu_command_command_builder(self, rest_client): assert result.type == commands.CommandType.MESSAGE def test_build_action_row(self, rest_client): - with mock.patch.object(special_endpoints, "ActionRowBuilder") as action_row_builder: + with mock.patch.object(special_endpoints, "MessageActionRowBuilder") as action_row_builder: assert rest_client.build_action_row() is action_row_builder.return_value action_row_builder.assert_called_once_with() + def test_build_message_action_row(self, rest_client): + with mock.patch.object(special_endpoints, "MessageActionRowBuilder") as action_row_builder: + assert rest_client.build_message_action_row() is action_row_builder.return_value + + action_row_builder.assert_called_once_with() + def test__build_message_payload_with_undefined_args(self, rest_client): with mock.patch.object( mentions, "generate_allowed_mentions", return_value={"allowed_mentions": 1} @@ -1554,12 +1560,10 @@ def test_interaction_modal_builder(self, rest_client): assert isinstance(result, special_endpoints.InteractionModalBuilder) def test_interaction_modal_builder_with_components(self, rest_client): - component = mock.Mock() - result = rest_client.interaction_modal_builder("title", "custom", components=(component,)) + result = rest_client.interaction_modal_builder("title", "custom") assert result.type == 9 assert isinstance(result, special_endpoints.InteractionModalBuilder) - assert result.components == [component] def test_fetch_scheduled_event_users(self, rest_client: rest.RESTClientImpl): with mock.patch.object(special_endpoints, "ScheduledEventUserIterator") as iterator_cls: diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py index 16b9e11f45..3cd57f7b29 100644 --- a/tests/hikari/impl/test_special_endpoints.py +++ b/tests/hikari/impl/test_special_endpoints.py @@ -24,6 +24,7 @@ import pytest from hikari import commands +from hikari import components from hikari import emojis from hikari import files from hikari import locales @@ -33,7 +34,6 @@ from hikari import undefined from hikari.impl import special_endpoints from hikari.interactions import base_interactions -from hikari.interactions import modal_interactions from hikari.internal import routes from tests.hikari import hikari_test_helpers @@ -1189,7 +1189,7 @@ class Test_ButtonBuilder: def button(self): return special_endpoints._ButtonBuilder( container=mock.Mock(), - style=messages.ButtonStyle.DANGER, + style=components.ButtonStyle.DANGER, custom_id="sfdasdasd", url="hi there", emoji=543123, @@ -1200,7 +1200,7 @@ def button(self): ) def test_style_property(self, button): - assert button.style is messages.ButtonStyle.DANGER + assert button.style is components.ButtonStyle.DANGER def test_emoji_property(self, button): assert button.emoji == 543123 @@ -1242,7 +1242,7 @@ def test_set_is_disabled(self, button): def test_build(self): result = special_endpoints._ButtonBuilder( container=object(), - style=messages.ButtonStyle.DANGER, + style=components.ButtonStyle.DANGER, url=undefined.UNDEFINED, emoji_id=undefined.UNDEFINED, emoji_name="emoji_name", @@ -1252,8 +1252,8 @@ def test_build(self): ).build() assert result == { - "type": messages.ComponentType.BUTTON, - "style": messages.ButtonStyle.DANGER, + "type": components.ComponentType.BUTTON, + "style": components.ButtonStyle.DANGER, "emoji": {"name": "emoji_name"}, "label": "no u", "custom_id": "ooga booga", @@ -1263,7 +1263,7 @@ def test_build(self): def test_build_without_optional_fields(self): result = special_endpoints._ButtonBuilder( container=object(), - style=messages.ButtonStyle.LINK, + style=components.ButtonStyle.LINK, url="OK", emoji_id="123321", emoji_name=undefined.UNDEFINED, @@ -1273,8 +1273,8 @@ def test_build_without_optional_fields(self): ).build() assert result == { - "type": messages.ComponentType.BUTTON, - "style": messages.ButtonStyle.LINK, + "type": components.ComponentType.BUTTON, + "style": components.ButtonStyle.LINK, "emoji": {"id": "123321"}, "disabled": False, "url": "OK", @@ -1284,7 +1284,7 @@ def test_add_to_container(self): mock_container = mock.Mock() button = special_endpoints._ButtonBuilder( container=mock_container, - style=messages.ButtonStyle.DANGER, + style=components.ButtonStyle.DANGER, url=undefined.UNDEFINED, emoji_id=undefined.UNDEFINED, emoji_name="emoji_name", @@ -1302,7 +1302,7 @@ class TestLinkButtonBuilder: def test_url_property(self): button = special_endpoints.LinkButtonBuilder( container=object(), - style=messages.ButtonStyle.DANGER, + style=components.ButtonStyle.DANGER, url="hihihihi", emoji_id=undefined.UNDEFINED, emoji_name="emoji_name", @@ -1318,7 +1318,7 @@ class TestInteractiveButtonBuilder: def test_custom_id_property(self): button = special_endpoints.InteractiveButtonBuilder( container=object(), - style=messages.ButtonStyle.DANGER, + style=components.ButtonStyle.DANGER, url="hihihihi", emoji_id=undefined.UNDEFINED, emoji_name="emoji_name", @@ -1465,7 +1465,7 @@ def test_build(self): result = special_endpoints.SelectMenuBuilder(container=object(), custom_id="o2o2o2").build() assert result == { - "type": messages.ComponentType.SELECT_MENU, + "type": components.ComponentType.SELECT_MENU, "custom_id": "o2o2o2", "options": [], "disabled": False, @@ -1485,7 +1485,7 @@ def test_build_partial(self): ) assert result == { - "type": messages.ComponentType.SELECT_MENU, + "type": components.ComponentType.SELECT_MENU, "custom_id": "o2o2o2", "options": [{"hi": "OK"}], "placeholder": "hi", @@ -1505,8 +1505,8 @@ def text_input(self): ) def test_set_style(self, text_input): - assert text_input.set_style(modal_interactions.TextInputStyle.PARAGRAPH) is text_input - assert text_input.style == modal_interactions.TextInputStyle.PARAGRAPH + assert text_input.set_style(components.TextInputStyle.PARAGRAPH) is text_input + assert text_input.style == components.TextInputStyle.PARAGRAPH def test_set_custom_id(self, text_input): assert text_input.set_custom_id("custooom") is text_input @@ -1548,7 +1548,7 @@ def test_build(self): ).build() assert result == { - "type": modal_interactions.ModalComponentType.TEXT_INPUT, + "type": components.ComponentType.TEXT_INPUT, "style": 1, "custom_id": "o2o2o2", "label": "label", @@ -1570,7 +1570,7 @@ def test_build_partial(self): ) assert result == { - "type": modal_interactions.ModalComponentType.TEXT_INPUT, + "type": components.ComponentType.TEXT_INPUT, "style": 1, "custom_id": "o2o2o2", "label": "label", @@ -1582,30 +1582,30 @@ def test_build_partial(self): } -class TestActionRowBuilder: +class TestMessageActionRowBuilder: def test_components_property(self): mock_component = object() - row = special_endpoints.ActionRowBuilder().add_component(mock_component) + row = special_endpoints.MessageActionRowBuilder().add_component(mock_component) assert row.components == [mock_component] def test_add_button_for_interactive(self): - row = special_endpoints.ActionRowBuilder() - button = row.add_button(messages.ButtonStyle.DANGER, "go home") + row = special_endpoints.MessageActionRowBuilder() + button = row.add_button(components.ButtonStyle.DANGER, "go home") button.add_to_container() assert row.components == [button] def test_add_button_for_link(self): - row = special_endpoints.ActionRowBuilder() - button = row.add_button(messages.ButtonStyle.LINK, "go home") + row = special_endpoints.MessageActionRowBuilder() + button = row.add_button(components.ButtonStyle.LINK, "go home") button.add_to_container() assert row.components == [button] def test_add_select_menu(self): - row = special_endpoints.ActionRowBuilder() + row = special_endpoints.MessageActionRowBuilder() menu = row.add_select_menu("hihihi") menu.add_to_container() @@ -1616,13 +1616,13 @@ def test_build(self): mock_component_1 = mock.Mock() mock_component_2 = mock.Mock() - row = special_endpoints.ActionRowBuilder() + row = special_endpoints.MessageActionRowBuilder() row._components = [mock_component_1, mock_component_2] result = row.build() assert result == { - "type": messages.ComponentType.ACTION_ROW, + "type": components.ComponentType.ACTION_ROW, "components": [mock_component_1.build.return_value, mock_component_2.build.return_value], } mock_component_1.build.assert_called_once_with() diff --git a/tests/hikari/interactions/test_base_interactions.py b/tests/hikari/interactions/test_base_interactions.py index 8652555b90..6aa2e712d6 100644 --- a/tests/hikari/interactions/test_base_interactions.py +++ b/tests/hikari/interactions/test_base_interactions.py @@ -212,23 +212,20 @@ def mock_modal_response_mixin(self, mock_app): @pytest.mark.asyncio() async def test_create_modal_response(self, mock_modal_response_mixin, mock_app): - await mock_modal_response_mixin.create_modal_response("title", "custom_id", []) + await mock_modal_response_mixin.create_modal_response("title", "custom_id", None, []) mock_app.rest.create_modal_response.assert_awaited_once_with( 34123, "399393939doodsodso", title="title", custom_id="custom_id", + component=None, components=[], ) def test_build_response(self, mock_modal_response_mixin, mock_app): mock_app.rest.interaction_modal_builder = mock.Mock() - builder = mock_modal_response_mixin.build_modal_response("title", "custom_id", components=[]) + builder = mock_modal_response_mixin.build_modal_response("title", "custom_id") assert builder is mock_app.rest.interaction_modal_builder.return_value - mock_app.rest.interaction_modal_builder.assert_called_once_with( - title="title", - custom_id="custom_id", - components=[], - ) + mock_app.rest.interaction_modal_builder.assert_called_once_with(title="title", custom_id="custom_id") diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py index 6330a6ebae..a539792640 100644 --- a/tests/hikari/interactions/test_modal_interactions.py +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -23,6 +23,7 @@ import pytest from hikari import channels +from hikari import components from hikari import snowflakes from hikari import traits from hikari.impl import special_endpoints @@ -54,10 +55,10 @@ def mock_modal_interaction(self, mock_app): locale="es-ES", guild_locale="en-US", app_permissions=543123, - components=special_endpoints.ActionRowBuilder( + components=special_endpoints.ModalActionRowBuilder( components=[ - modal_interactions.InteractionTextInput( - type=modal_interactions.ModalComponentType.TEXT_INPUT, custom_id="le id", value="le value" + components.TextInputComponent( + type=components.ComponentType.TEXT_INPUT, custom_id="le id", value="le value" ) ], ), diff --git a/tests/hikari/test_components.py b/tests/hikari/test_components.py new file mode 100644 index 0000000000..4e734a0253 --- /dev/null +++ b/tests/hikari/test_components.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from hikari import components + + +class TestActionRowComponent: + def test_getitem_operator_with_index(self): + mock_component = object() + row = components.ActionRowComponent(type=1, components=[object(), mock_component, object()]) + + assert row[1] is mock_component + + def test_getitem_operator_with_slice(self): + mock_component_1 = object() + mock_component_2 = object() + row = components.ActionRowComponent(type=1, components=[object(), mock_component_1, object(), mock_component_2]) + + assert row[1:4:2] == [mock_component_1, mock_component_2] + + def test_iter_operator(self): + mock_component_1 = object() + mock_component_2 = object() + row = components.ActionRowComponent(type=1, components=[mock_component_1, mock_component_2]) + + assert list(row) == [mock_component_1, mock_component_2] + + def test_len_operator(self): + row = components.ActionRowComponent(type=1, components=[object(), object()]) + + assert len(row) == 2 diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index 5738a8f50f..dc55ef54ab 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -87,33 +87,6 @@ def test_make_cover_image_url_when_hash_is_not_none(self, message_application): ) -class TestActionRowComponent: - def test_getitem_operator_with_index(self): - mock_component = object() - row = messages.ActionRowComponent(type=1, components=[object(), mock_component, object()]) - - assert row[1] is mock_component - - def test_getitem_operator_with_slice(self): - mock_component_1 = object() - mock_component_2 = object() - row = messages.ActionRowComponent(type=1, components=[object(), mock_component_1, object(), mock_component_2]) - - assert row[1:4:2] == [mock_component_1, mock_component_2] - - def test_iter_operator(self): - mock_component_1 = object() - mock_component_2 = object() - row = messages.ActionRowComponent(type=1, components=[mock_component_1, mock_component_2]) - - assert list(row) == [mock_component_1, mock_component_2] - - def test_len_operator(self): - row = messages.ActionRowComponent(type=1, components=[object(), object()]) - - assert len(row) == 2 - - @pytest.fixture() def message(): return messages.Message( From ccbe0411f0b3bcf1fe24bc8700d8c360c0514c5b Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 20 Nov 2022 18:53:04 +0100 Subject: [PATCH 34/40] Generalize modals --- changes/1002.feature.md | 1 + hikari/components.py | 19 ++-- hikari/impl/entity_factory.py | 102 ++++++++++++++++------ hikari/interactions/modal_interactions.py | 2 +- hikari/internal/cache.py | 2 +- hikari/messages.py | 6 +- 6 files changed, 96 insertions(+), 36 deletions(-) diff --git a/changes/1002.feature.md b/changes/1002.feature.md index 24d0f6c27a..888c73d716 100644 --- a/changes/1002.feature.md +++ b/changes/1002.feature.md @@ -1 +1,2 @@ Implement modal interactions. +- Additionally, it is now guaranteed (typing-wise) that top level components will be an action row diff --git a/hikari/components.py b/hikari/components.py index 60e9926aa4..9f58489aee 100644 --- a/hikari/components.py +++ b/hikari/components.py @@ -137,11 +137,14 @@ class PartialComponent: """The type of component this is.""" +AllowedComponentsT = typing.TypeVar("AllowedComponentsT", bound="PartialComponent") + + @attr.define(weakref_slot=False) -class ActionRowComponent(PartialComponent): +class ActionRowComponent(typing.Generic[AllowedComponentsT], PartialComponent): """Represents a row of components.""" - components: typing.Sequence[PartialComponent] = attr.field() + components: typing.Sequence[AllowedComponentsT] = attr.field() """Sequence of the components contained within this row.""" @typing.overload @@ -149,15 +152,15 @@ def __getitem__(self, index: int, /) -> PartialComponent: ... @typing.overload - def __getitem__(self, slice_: slice, /) -> typing.Sequence[PartialComponent]: + def __getitem__(self, slice_: slice, /) -> typing.Sequence[AllowedComponentsT]: ... def __getitem__( self, index_or_slice: typing.Union[int, slice], / - ) -> typing.Union[PartialComponent, typing.Sequence[PartialComponent]]: + ) -> typing.Union[PartialComponent, typing.Sequence[AllowedComponentsT]]: return self.components[index_or_slice] - def __iter__(self) -> typing.Iterator[PartialComponent]: + def __iter__(self) -> typing.Iterator[AllowedComponentsT]: return iter(self.components) def __len__(self) -> int: @@ -290,3 +293,9 @@ class TextInputComponent(PartialComponent): * `ButtonStyle.SUCCESS` * `ButtonStyle.DANGER` """ + +MessageComponentTypesT = typing.Union[ButtonComponent, SelectMenuComponent] +ModalComponentTypesT = TextInputComponent + +MessageActionRowComponentT = ActionRowComponent[MessageComponentTypesT] +ModalActionRowComponentT = ActionRowComponent[ModalComponentTypesT] diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index f5d15acceb..774c1d381c 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -433,7 +433,8 @@ class EntityFactoryImpl(entity_factory.EntityFactory): "_audit_log_event_mapping", "_command_mapping", "_modal_component_type_mapping", - "_component_type_mapping", + "_message_component_type_mapping", + "_modal_component_type_mapping", "_dm_channel_type_mapping", "_guild_channel_type_mapping", "_thread_channel_type_mapping", @@ -505,10 +506,15 @@ def __init__(self, app: traits.RESTAware) -> None: commands.CommandType.USER: self.deserialize_context_menu_command, commands.CommandType.MESSAGE: self.deserialize_context_menu_command, } - self._component_type_mapping = { - component_models.ComponentType.ACTION_ROW: self._deserialize_action_row, + self._message_component_type_mapping: typing.Dict[ + 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, + } + self._modal_component_type_mapping: typing.Dict[ + int, typing.Callable[[data_binding.JSONObject], component_models.ModalComponentTypesT] + ] = { component_models.ComponentType.TEXT_INPUT: self._deserialize_text_input, } self._dm_channel_type_mapping = { @@ -2445,13 +2451,6 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod member = None user = self.deserialize_user(payload["user"]) - components: typing.List[component_models.PartialComponent] = [] - for component_payload in data_payload["components"]: - try: - components.append(self._deserialize_component(component_payload)) - except errors.UnrecognisedEntityError: - pass - message: typing.Optional[message_models.Message] = None if message_payload := payload.get("message"): message = self.deserialize_message(message_payload) @@ -2472,7 +2471,7 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod token=payload["token"], version=payload["version"], custom_id=data_payload["custom_id"], - components=components, + components=self._deserialize_modal_components(data_payload["components"]), message=message, ) @@ -2609,24 +2608,67 @@ def deserialize_guild_sticker(self, payload: data_binding.JSONObject) -> sticker user=self.deserialize_user(payload["user"]) if "user" in payload else None, ) - ################## - # MESSAGE MODELS # - ################## + #################### + # COMPONENT MODELS # + #################### - def _deserialize_component(self, payload: data_binding.JSONObject) -> component_models.PartialComponent: + def _deserialize_message_component( + self, payload: data_binding.JSONObject + ) -> component_models.MessageComponentTypesT: component_type = component_models.ComponentType(payload["type"]) - if deserialize := self._component_type_mapping.get(component_type): + if deserialize := self._message_component_type_mapping.get(component_type): return deserialize(payload) - _LOGGER.debug("Unknown component type %s", component_type) - raise errors.UnrecognisedEntityError(f"Unrecognised component type {component_type}") + _LOGGER.debug("Unknown message component type %s", component_type) + raise errors.UnrecognisedEntityError(f"Unrecognised message component type {component_type}") - def _deserialize_action_row(self, payload: data_binding.JSONObject) -> component_models.ActionRowComponent: - components = data_binding.cast_variants_array(self._deserialize_component, payload["components"]) - return component_models.ActionRowComponent( - type=component_models.ComponentType(payload["type"]), components=components - ) + def _deserialize_modal_component(self, payload: data_binding.JSONObject) -> component_models.ModalComponentTypesT: + component_type = component_models.ComponentType(payload["type"]) + + if deserialize := self._modal_component_type_mapping.get(component_type): + return deserialize(payload) + + _LOGGER.debug("Unknown modal component type %s", component_type) + raise errors.UnrecognisedEntityError(f"Unrecognised modal component type {component_type}") + + def _deserialize_message_components( + self, payloads: data_binding.JSONArray + ) -> typing.List[component_models.MessageActionRowComponentT]: + top_level_components = [] + + for payload in payloads: + top_level_component_type = component_models.ComponentType(payload["type"]) + + if top_level_component_type != component_models.ComponentType.ACTION_ROW: + _LOGGER.debug("Unknown top-level message component type %s", top_level_component_type) + continue + + components = data_binding.cast_variants_array(self._deserialize_message_component, payload["components"]) + top_level_components.append( + component_models.ActionRowComponent(type=top_level_component_type, components=components) + ) + + return top_level_components + + def _deserialize_modal_components( + self, payloads: data_binding.JSONArray + ) -> typing.List[component_models.ModalActionRowComponentT]: + top_level_components = [] + + for payload in payloads: + top_level_component_type = component_models.ComponentType(payload["type"]) + + if top_level_component_type != component_models.ComponentType.ACTION_ROW: + _LOGGER.debug("Unknown top-level modal component type %s", top_level_component_type) + continue + + components = data_binding.cast_variants_array(self._deserialize_modal_component, payload["components"]) + top_level_components.append( + component_models.ActionRowComponent(type=top_level_component_type, components=components) + ) + + return top_level_components def _deserialize_button(self, payload: data_binding.JSONObject) -> component_models.ButtonComponent: emoji_payload = payload.get("emoji") @@ -2674,6 +2716,10 @@ def _deserialize_text_input(self, payload: data_binding.JSONObject) -> component value=payload["value"], ) + ################## + # MESSAGE MODELS # + ################## + def _deserialize_message_activity(self, payload: data_binding.JSONObject) -> message_models.MessageActivity: return message_models.MessageActivity( type=message_models.MessageActivityType(payload["type"]), party_id=payload.get("party_id") @@ -2810,9 +2856,11 @@ def deserialize_partial_message( # noqa CFQ001 - Function too long if interaction_payload := payload.get("interaction"): interaction = self._deserialize_message_interaction(interaction_payload) - components: undefined.UndefinedOr[typing.List[component_models.PartialComponent]] = undefined.UNDEFINED + components: undefined.UndefinedOr[ + typing.List[component_models.MessageActionRowComponentT] + ] = undefined.UNDEFINED if component_payloads := payload.get("components"): - components = data_binding.cast_variants_array(self._deserialize_component, component_payloads) + components = self._deserialize_message_components(component_payloads) channel_mentions: undefined.UndefinedOr[ typing.Dict[snowflakes.Snowflake, channel_models.PartialChannel] @@ -2914,9 +2962,9 @@ def deserialize_message( if interaction_payload := payload.get("interaction"): interaction = self._deserialize_message_interaction(interaction_payload) - components: typing.List[component_models.PartialComponent] = [] + components: typing.List[component_models.MessageActionRowComponentT] if component_payloads := payload.get("components"): - components = data_binding.cast_variants_array(self._deserialize_component, component_payloads) + components = self._deserialize_message_components(component_payloads) else: components = [] diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index e062e9614f..e3513240c2 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -121,7 +121,7 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes app_permissions: typing.Optional[permissions.Permissions] = attr.field(eq=False, hash=False, repr=False) """Permissions the bot has in this interaction's channel if it's in a guild.""" - components: typing.Sequence[components_.PartialComponent] = attr.field(eq=False, hash=False, repr=True) + components: typing.Sequence[components_.ModalActionRowComponentT] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" async def fetch_channel(self) -> channels.TextableChannel: diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py index 351692590a..28f8ed5bef 100644 --- a/hikari/internal/cache.py +++ b/hikari/internal/cache.py @@ -754,7 +754,7 @@ class MessageData(BaseData[messages.Message]): referenced_message: typing.Optional[RefCell[MessageData]] = attr.field() interaction: typing.Optional[MessageInteractionData] = attr.field() application_id: typing.Optional[snowflakes.Snowflake] = attr.field() - components: typing.Tuple[components_.PartialComponent, ...] = attr.field() + components: typing.Tuple[components_.MessageActionRowComponentT, ...] = attr.field() @classmethod def build_from_entity( diff --git a/hikari/messages.py b/hikari/messages.py index bf05550861..d6a9352f29 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -565,7 +565,7 @@ class PartialMessage(snowflakes.Unique): This will only be provided for interaction messages. """ - components: undefined.UndefinedOr[typing.Sequence[component_models.PartialComponent]] = attr.field( + components: undefined.UndefinedOr[typing.Sequence[component_models.MessageActionRowComponentT]] = attr.field( hash=False, eq=False, repr=False ) """Sequence of the components attached to this message.""" @@ -1410,5 +1410,7 @@ class Message(PartialMessage): This will only be provided for interaction messages. """ - components: typing.Sequence[component_models.PartialComponent] = attr.field(hash=False, eq=False, repr=False) + components: typing.Sequence[component_models.MessageActionRowComponentT] = attr.field( + hash=False, eq=False, repr=False + ) """Sequence of the components attached to this message.""" From baf90700df4c7fc7015916773791a4c98f225c5d Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 20 Nov 2022 20:17:09 +0100 Subject: [PATCH 35/40] Fix tests --- hikari/impl/entity_factory.py | 78 +++++++++++------------- tests/hikari/impl/test_entity_factory.py | 73 +++++++++++++--------- 2 files changed, 79 insertions(+), 72 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 774c1d381c..64e5262cbf 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -2471,7 +2471,7 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod token=payload["token"], version=payload["version"], custom_id=data_payload["custom_id"], - components=self._deserialize_modal_components(data_payload["components"]), + components=self._deserialize_components(data_payload["components"], self._modal_component_type_mapping), message=message, ) @@ -2612,29 +2612,27 @@ def deserialize_guild_sticker(self, payload: data_binding.JSONObject) -> sticker # COMPONENT MODELS # #################### - def _deserialize_message_component( - self, payload: data_binding.JSONObject - ) -> component_models.MessageComponentTypesT: - component_type = component_models.ComponentType(payload["type"]) - - if deserialize := self._message_component_type_mapping.get(component_type): - return deserialize(payload) - - _LOGGER.debug("Unknown message component type %s", component_type) - raise errors.UnrecognisedEntityError(f"Unrecognised message component type {component_type}") - - def _deserialize_modal_component(self, payload: data_binding.JSONObject) -> component_models.ModalComponentTypesT: - component_type = component_models.ComponentType(payload["type"]) - - if deserialize := self._modal_component_type_mapping.get(component_type): - return deserialize(payload) + @typing.overload + def _deserialize_components( + self, + payloads: data_binding.JSONArray, + mapping: typing.Dict[int, typing.Callable[[data_binding.JSONObject], component_models.MessageComponentTypesT]], + ) -> typing.List[component_models.MessageActionRowComponentT]: + ... - _LOGGER.debug("Unknown modal component type %s", component_type) - raise errors.UnrecognisedEntityError(f"Unrecognised modal component type {component_type}") + @typing.overload + def _deserialize_components( + self, + payloads: data_binding.JSONArray, + mapping: typing.Dict[int, typing.Callable[[data_binding.JSONObject], component_models.ModalComponentTypesT]], + ) -> typing.List[component_models.ModalActionRowComponentT]: + ... - def _deserialize_message_components( - self, payloads: data_binding.JSONArray - ) -> typing.List[component_models.MessageActionRowComponentT]: + def _deserialize_components( + self, + payloads: data_binding.JSONArray, + mapping: typing.Dict[int, typing.Callable[[data_binding.JSONObject], typing.Any]], + ) -> typing.List[component_models.ActionRowComponent[typing.Any]]: top_level_components = [] for payload in payloads: @@ -2644,29 +2642,23 @@ def _deserialize_message_components( _LOGGER.debug("Unknown top-level message component type %s", top_level_component_type) continue - components = data_binding.cast_variants_array(self._deserialize_message_component, payload["components"]) - top_level_components.append( - component_models.ActionRowComponent(type=top_level_component_type, components=components) - ) + components = [] - return top_level_components + for component_payload in payload["components"]: + component_type = component_models.ComponentType(component_payload["type"]) - def _deserialize_modal_components( - self, payloads: data_binding.JSONArray - ) -> typing.List[component_models.ModalActionRowComponentT]: - top_level_components = [] - - for payload in payloads: - top_level_component_type = component_models.ComponentType(payload["type"]) + if (deserializer := mapping.get(component_type)) is None: + _LOGGER.debug("Unknown component type %s", component_type) + continue - if top_level_component_type != component_models.ComponentType.ACTION_ROW: - _LOGGER.debug("Unknown top-level modal component type %s", top_level_component_type) - continue + components.append(deserializer(component_payload)) - components = data_binding.cast_variants_array(self._deserialize_modal_component, payload["components"]) - top_level_components.append( - component_models.ActionRowComponent(type=top_level_component_type, components=components) - ) + if components: + # If we somehow get a top-level component full of unknown components, ignore the top-level + # component all-together + top_level_components.append( + component_models.ActionRowComponent(type=top_level_component_type, components=components) + ) return top_level_components @@ -2860,7 +2852,7 @@ def deserialize_partial_message( # noqa CFQ001 - Function too long typing.List[component_models.MessageActionRowComponentT] ] = undefined.UNDEFINED if component_payloads := payload.get("components"): - components = self._deserialize_message_components(component_payloads) + components = self._deserialize_components(component_payloads, self._message_component_type_mapping) channel_mentions: undefined.UndefinedOr[ typing.Dict[snowflakes.Snowflake, channel_models.PartialChannel] @@ -2964,7 +2956,7 @@ def deserialize_message( components: typing.List[component_models.MessageActionRowComponentT] if component_payloads := payload.get("components"): - components = self._deserialize_message_components(component_payloads) + components = self._deserialize_components(component_payloads, self._message_component_type_mapping) else: components = [] diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 19ace6064a..49ad8e00de 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -5075,27 +5075,14 @@ def test_max_age_when_zero(self, entity_factory_impl, invite_with_metadata_paylo invite_with_metadata_payload["max_age"] = 0 assert entity_factory_impl.deserialize_invite_with_metadata(invite_with_metadata_payload).max_age is None - ################## - # MESSAGE MODELS # - ################## + #################### + # COMPONENT MODELS # + #################### @pytest.fixture() def action_row_payload(self, button_payload): return {"type": 1, "components": [button_payload]} - def test__deserialize_action_row(self, entity_factory_impl, action_row_payload, button_payload): - action_row = entity_factory_impl._deserialize_action_row(action_row_payload) - - assert action_row.type is component_models.ComponentType.ACTION_ROW - assert action_row.components == [entity_factory_impl._deserialize_component(button_payload)] - - def test__deserialize_action_row_handles_unknown_component_type(self, entity_factory_impl): - action_row = entity_factory_impl._deserialize_action_row( - {"type": 1, "components": [{"type": "9494949"}, {"type": "9239292"}]} - ) - - assert action_row.components == [] - @pytest.fixture() def button_payload(self, custom_emoji_payload): return { @@ -5195,28 +5182,52 @@ def test__deserialize_select_menu_partial(self, entity_factory_impl): assert menu.is_disabled is False @pytest.mark.parametrize( - ("type_", "fn"), + ("type_", "fn", "mapping"), [ - (1, "_deserialize_action_row"), - (2, "_deserialize_button"), - (3, "_deserialize_select_menu"), + (2, "_deserialize_button", "_message_component_type_mapping"), + (3, "_deserialize_select_menu", "_message_component_type_mapping"), + (4, "_deserialize_text_input", "_modal_component_type_mapping"), ], ) - def test__deserialize_component(self, mock_app, type_, fn): - payload = {"type": type_} + def test__deserialize_components(self, mock_app, type_, fn, mapping): + component_payload = {"type": type_} + payload = [{"type": 1, "components": [component_payload]}] with mock.patch.object(entity_factory.EntityFactoryImpl, fn) as expected_fn: # We need to instantiate it after the mock so that the functions that are stored in the dicts # are the ones we mock entity_factory_impl = entity_factory.EntityFactoryImpl(app=mock_app) - assert entity_factory_impl._deserialize_component(payload) is expected_fn.return_value + components = entity_factory_impl._deserialize_components(payload, getattr(entity_factory_impl, mapping)) - expected_fn.assert_called_once_with(payload) + expected_fn.assert_called_once_with(component_payload) + action_row = components[0] + assert isinstance(action_row, component_models.ActionRowComponent) + assert action_row.components[0] is expected_fn.return_value - def test__deserialize_component_handles_unknown_type(self, entity_factory_impl): - with pytest.raises(errors.UnrecognisedEntityError): - entity_factory_impl._deserialize_component({"type": -9434994}) + def test__deserialize_components_handles_unknown_top_component_type(self, entity_factory_impl): + components = entity_factory_impl._deserialize_components( + [ + # Unknown top-level component + {"type": -9434994}, + { + # Known top-level component + "type": 1, + "components": [ + # Unknown components + {"type": 1}, + {"type": 1000000}, + ], + }, + ], + {}, + ) + + assert components == [] + + ################## + # MESSAGE MODELS # + ################## @pytest.fixture() def partial_application_payload(self): @@ -5452,7 +5463,9 @@ def test_deserialize_partial_message( assert partial_message.interaction.user == entity_factory_impl.deserialize_user(user_payload) assert isinstance(partial_message.interaction, message_models.MessageInteraction) - assert partial_message.components == [entity_factory_impl._deserialize_component(action_row_payload)] + assert partial_message.components == entity_factory_impl._deserialize_components( + [action_row_payload], entity_factory_impl._message_component_type_mapping + ) def test_deserialize_partial_message_with_partial_fields(self, entity_factory_impl, message_payload): message_payload["content"] = "" @@ -5632,7 +5645,9 @@ def test_deserialize_message( assert message.interaction.user == entity_factory_impl.deserialize_user(user_payload) assert isinstance(message.interaction, message_models.MessageInteraction) - assert message.components == [entity_factory_impl._deserialize_component(action_row_payload)] + assert message.components == entity_factory_impl._deserialize_components( + [action_row_payload], entity_factory_impl._message_component_type_mapping + ) def test_deserialize_message_with_unset_sub_fields(self, entity_factory_impl, message_payload): del message_payload["application"]["cover_image"] From c810e41665f695273eb7090c51b1e0d1004a8ee2 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 20 Nov 2022 20:22:51 +0100 Subject: [PATCH 36/40] Documentation and exposing typehints --- hikari/components.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/hikari/components.py b/hikari/components.py index 9f58489aee..c0779a739d 100644 --- a/hikari/components.py +++ b/hikari/components.py @@ -36,6 +36,10 @@ "TextInputComponent", "InteractiveButtonTypes", "InteractiveButtonTypesT", + "MessageComponentTypesT", + "ModalComponentTypesT", + "MessageActionRowComponentT", + "ModalActionRowComponentT", ) import typing @@ -295,7 +299,22 @@ class TextInputComponent(PartialComponent): """ MessageComponentTypesT = typing.Union[ButtonComponent, SelectMenuComponent] +"""Type hint of the `PartialComponent`s that be contained in a `MessageActionRowComponentT`. + +The following values are valid for this: + +* `ButtonComponent` +* `SelectMenuComponent` +""" ModalComponentTypesT = TextInputComponent +"""Type hint of the `PartialComponent`s that be contained in a `ModalActionRowComponentT`. + +The following values are valid for this: + +* `TextInputComponent` +""" MessageActionRowComponentT = ActionRowComponent[MessageComponentTypesT] +"""Typehint for a message action row component.""" ModalActionRowComponentT = ActionRowComponent[ModalComponentTypesT] +"""Typehint for a modal action row component.""" From a118854617cfc2ac4c9a629881d95da16556f2aa Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 20 Nov 2022 20:28:41 +0100 Subject: [PATCH 37/40] Remove duplicate slot --- hikari/impl/entity_factory.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 64e5262cbf..9cde7dff17 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -432,7 +432,6 @@ class EntityFactoryImpl(entity_factory.EntityFactory): "_audit_log_entry_converters", "_audit_log_event_mapping", "_command_mapping", - "_modal_component_type_mapping", "_message_component_type_mapping", "_modal_component_type_mapping", "_dm_channel_type_mapping", From 08ff5fcc4eca55bb26ae2f2d9d369bbd6fe25834 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 4 Dec 2022 22:21:29 +0100 Subject: [PATCH 38/40] Switch message action row types to class specializations --- hikari/components.py | 16 ++++++++-------- hikari/impl/entity_factory.py | 10 ++++------ hikari/interactions/modal_interactions.py | 2 +- hikari/internal/cache.py | 2 +- hikari/messages.py | 4 ++-- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/hikari/components.py b/hikari/components.py index c0779a739d..d21af5f69d 100644 --- a/hikari/components.py +++ b/hikari/components.py @@ -38,8 +38,8 @@ "InteractiveButtonTypesT", "MessageComponentTypesT", "ModalComponentTypesT", - "MessageActionRowComponentT", - "ModalActionRowComponentT", + "MessageActionRowComponent", + "ModalActionRowComponent", ) import typing @@ -299,7 +299,7 @@ class TextInputComponent(PartialComponent): """ MessageComponentTypesT = typing.Union[ButtonComponent, SelectMenuComponent] -"""Type hint of the `PartialComponent`s that be contained in a `MessageActionRowComponentT`. +"""Type hint of the `PartialComponent`s that be contained in a `MessageActionRowComponent`. The following values are valid for this: @@ -307,14 +307,14 @@ class TextInputComponent(PartialComponent): * `SelectMenuComponent` """ ModalComponentTypesT = TextInputComponent -"""Type hint of the `PartialComponent`s that be contained in a `ModalActionRowComponentT`. +"""Type hint of the `PartialComponent`s that be contained in a `ModalActionRowComponent`. The following values are valid for this: * `TextInputComponent` """ -MessageActionRowComponentT = ActionRowComponent[MessageComponentTypesT] -"""Typehint for a message action row component.""" -ModalActionRowComponentT = ActionRowComponent[ModalComponentTypesT] -"""Typehint for a modal action row component.""" +MessageActionRowComponent = ActionRowComponent[MessageComponentTypesT] +"""A message action row component.""" +ModalActionRowComponent = ActionRowComponent[ModalComponentTypesT] +"""A modal action row component.""" diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 9cde7dff17..d4d447ca0a 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -2616,7 +2616,7 @@ def _deserialize_components( self, payloads: data_binding.JSONArray, mapping: typing.Dict[int, typing.Callable[[data_binding.JSONObject], component_models.MessageComponentTypesT]], - ) -> typing.List[component_models.MessageActionRowComponentT]: + ) -> typing.List[component_models.MessageActionRowComponent]: ... @typing.overload @@ -2624,7 +2624,7 @@ def _deserialize_components( self, payloads: data_binding.JSONArray, mapping: typing.Dict[int, typing.Callable[[data_binding.JSONObject], component_models.ModalComponentTypesT]], - ) -> typing.List[component_models.ModalActionRowComponentT]: + ) -> typing.List[component_models.ModalActionRowComponent]: ... def _deserialize_components( @@ -2847,9 +2847,7 @@ def deserialize_partial_message( # noqa CFQ001 - Function too long if interaction_payload := payload.get("interaction"): interaction = self._deserialize_message_interaction(interaction_payload) - components: undefined.UndefinedOr[ - typing.List[component_models.MessageActionRowComponentT] - ] = undefined.UNDEFINED + components: undefined.UndefinedOr[typing.List[component_models.MessageActionRowComponent]] = undefined.UNDEFINED if component_payloads := payload.get("components"): components = self._deserialize_components(component_payloads, self._message_component_type_mapping) @@ -2953,7 +2951,7 @@ def deserialize_message( if interaction_payload := payload.get("interaction"): interaction = self._deserialize_message_interaction(interaction_payload) - components: typing.List[component_models.MessageActionRowComponentT] + components: typing.List[component_models.MessageActionRowComponent] if component_payloads := payload.get("components"): components = self._deserialize_components(component_payloads, self._message_component_type_mapping) diff --git a/hikari/interactions/modal_interactions.py b/hikari/interactions/modal_interactions.py index e3513240c2..54735b31de 100644 --- a/hikari/interactions/modal_interactions.py +++ b/hikari/interactions/modal_interactions.py @@ -121,7 +121,7 @@ class ModalInteraction(base_interactions.MessageResponseMixin[ModalResponseTypes app_permissions: typing.Optional[permissions.Permissions] = attr.field(eq=False, hash=False, repr=False) """Permissions the bot has in this interaction's channel if it's in a guild.""" - components: typing.Sequence[components_.ModalActionRowComponentT] = attr.field(eq=False, hash=False, repr=True) + components: typing.Sequence[components_.ModalActionRowComponent] = attr.field(eq=False, hash=False, repr=True) """Components in the modal.""" async def fetch_channel(self) -> channels.TextableChannel: diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py index 28f8ed5bef..c71ccb6f0f 100644 --- a/hikari/internal/cache.py +++ b/hikari/internal/cache.py @@ -754,7 +754,7 @@ class MessageData(BaseData[messages.Message]): referenced_message: typing.Optional[RefCell[MessageData]] = attr.field() interaction: typing.Optional[MessageInteractionData] = attr.field() application_id: typing.Optional[snowflakes.Snowflake] = attr.field() - components: typing.Tuple[components_.MessageActionRowComponentT, ...] = attr.field() + components: typing.Tuple[components_.MessageActionRowComponent, ...] = attr.field() @classmethod def build_from_entity( diff --git a/hikari/messages.py b/hikari/messages.py index d6a9352f29..39344a8631 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -565,7 +565,7 @@ class PartialMessage(snowflakes.Unique): This will only be provided for interaction messages. """ - components: undefined.UndefinedOr[typing.Sequence[component_models.MessageActionRowComponentT]] = attr.field( + components: undefined.UndefinedOr[typing.Sequence[component_models.MessageActionRowComponent]] = attr.field( hash=False, eq=False, repr=False ) """Sequence of the components attached to this message.""" @@ -1410,7 +1410,7 @@ class Message(PartialMessage): This will only be provided for interaction messages. """ - components: typing.Sequence[component_models.MessageActionRowComponentT] = attr.field( + components: typing.Sequence[component_models.MessageActionRowComponent] = attr.field( hash=False, eq=False, repr=False ) """Sequence of the components attached to this message.""" From 03f3f72cf418359b43a8813f17fee138e82de8ff Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 4 Dec 2022 22:27:48 +0100 Subject: [PATCH 39/40] Add missing test cases --- tests/hikari/impl/test_rest.py | 44 ++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 7c842fce09..57121b9605 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -1210,6 +1210,12 @@ def test_build_message_action_row(self, rest_client): action_row_builder.assert_called_once_with() + def test_build_modal_action_row(self, rest_client): + with mock.patch.object(special_endpoints, "ModalActionRowBuilder") as action_row_builder: + assert rest_client.build_modal_action_row() is action_row_builder.return_value + + action_row_builder.assert_called_once_with() + def test__build_message_payload_with_undefined_args(self, rest_client): with mock.patch.object( mentions, "generate_allowed_mentions", return_value={"allowed_mentions": 1} @@ -5900,11 +5906,11 @@ async def test_delete_interaction_response(self, rest_client): rest_client._request.assert_awaited_once_with(expected_route, no_auth=True) async def test_create_autocomplete_response(self, rest_client): - expected_route = routes.POST_INTERACTION_RESPONSE.compile(interaction=1235431, token="dissssnake") + expected_route = routes.POST_INTERACTION_RESPONSE.compile(interaction=1235431, token="snek") rest_client._request = mock.AsyncMock() choices = [commands.CommandChoice(name="a", value="b"), commands.CommandChoice(name="foo", value="bar")] - await rest_client.create_autocomplete_response(StubModel(1235431), "dissssnake", choices) + await rest_client.create_autocomplete_response(StubModel(1235431), "snek", choices) rest_client._request.assert_awaited_once_with( expected_route, @@ -5913,19 +5919,47 @@ async def test_create_autocomplete_response(self, rest_client): ) async def test_create_modal_response(self, rest_client): - expected_route = routes.POST_INTERACTION_RESPONSE.compile(interaction=1235431, token="dissssnake") + expected_route = routes.POST_INTERACTION_RESPONSE.compile(interaction=1235431, token="snek") rest_client._request = mock.AsyncMock() + component = mock.Mock() await rest_client.create_modal_response( - StubModel(1235431), "dissssnake", title="title", custom_id="idd", components=[] + StubModel(1235431), "snek", title="title", custom_id="idd", component=component ) rest_client._request.assert_awaited_once_with( expected_route, - json={"type": 9, "data": {"title": "title", "custom_id": "idd", "components": []}}, + json={ + "type": 9, + "data": {"title": "title", "custom_id": "idd", "components": [component.build.return_value]}, + }, no_auth=True, ) + async def test_create_modal_response_with_plural_args(self, rest_client): + expected_route = routes.POST_INTERACTION_RESPONSE.compile(interaction=1235431, token="snek") + rest_client._request = mock.AsyncMock() + component = mock.Mock() + + await rest_client.create_modal_response( + StubModel(1235431), "snek", title="title", custom_id="idd", components=[component] + ) + + rest_client._request.assert_awaited_once_with( + expected_route, + json={ + "type": 9, + "data": {"title": "title", "custom_id": "idd", "components": [component.build.return_value]}, + }, + no_auth=True, + ) + + async def test_create_modal_response_when_both_component_and_components_passed(self, rest_client): + with pytest.raises(ValueError): + await rest_client.create_modal_response( + StubModel(1235431), "snek", title="title", custom_id="idd", component="not none", components=[] + ) + async def test_fetch_scheduled_event(self, rest_client: rest.RESTClientImpl): expected_route = routes.GET_GUILD_SCHEDULED_EVENT.compile(guild=453123, scheduled_event=222332323) rest_client._request = mock.AsyncMock(return_value={"id": "4949494949"}) From 5c9053a93ada74043daaf2ebf8da81e734f7d4de Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 4 Dec 2022 22:36:25 +0100 Subject: [PATCH 40/40] Fix flake8 issue --- hikari/impl/rest.py | 2 +- tests/hikari/impl/test_rest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 513f338f44..074b5c8233 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -3793,7 +3793,7 @@ async def create_modal_response( components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED, ) -> None: if undefined.all_undefined(component, components) or not undefined.any_undefined(component, components): - raise ValueError("Must specify exactly only one of component or components") + raise ValueError("Must specify exactly only one of 'component' or 'components'") route = routes.POST_INTERACTION_RESPONSE.compile(interaction=interaction, token=token) diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index 57121b9605..2af8b2f925 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -5955,7 +5955,7 @@ async def test_create_modal_response_with_plural_args(self, rest_client): ) async def test_create_modal_response_when_both_component_and_components_passed(self, rest_client): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Must specify exactly only one of 'component' or 'components'"): await rest_client.create_modal_response( StubModel(1235431), "snek", title="title", custom_id="idd", component="not none", components=[] )