From 5009c83bc9fe866a03a2dbfc2042775d114ab1fc Mon Sep 17 00:00:00 2001 From: Trevor <75498301+NextChai@users.noreply.github.com> Date: Thu, 27 Oct 2022 10:03:45 -0400 Subject: [PATCH] Implement New Select Types Co-authored-by: Soheab_ <33902984+Soheab@users.noreply.github.com> Co-authored-by: rdrescher909 <51489753+rdrescher909@users.noreply.github.com> Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/components.py | 21 +- discord/enums.py | 5 + discord/types/components.py | 33 +- discord/types/interactions.py | 3 +- discord/ui/item.py | 2 +- discord/ui/modal.py | 8 +- discord/ui/select.py | 650 +++++++++++++++++++++++++++++----- discord/ui/text_input.py | 3 +- discord/ui/view.py | 2 +- docs/interactions/api.rst | 70 +++- 10 files changed, 692 insertions(+), 105 deletions(-) diff --git a/discord/components.py b/discord/components.py index 4993009ce..c0a213efa 100644 --- a/discord/components.py +++ b/discord/components.py @@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations from typing import ClassVar, List, Literal, Optional, TYPE_CHECKING, Tuple, Union, overload -from .enums import try_enum, ComponentType, ButtonStyle, TextStyle +from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, ChannelType from .utils import get_slots, MISSING from .partial_emoji import PartialEmoji, _EmojiTag @@ -234,6 +234,8 @@ class SelectMenu(Component): Attributes ------------ + type: :class:`ComponentType` + The type of component. custom_id: Optional[:class:`str`] The ID of the select menu that gets received during an interaction. placeholder: Optional[:class:`str`] @@ -248,31 +250,32 @@ class SelectMenu(Component): A list of options that can be selected in this menu. disabled: :class:`bool` Whether the select is disabled or not. + channel_types: List[:class:`.ChannelType`] + A list of channel types that are allowed to be chosen in this select menu. """ __slots__: Tuple[str, ...] = ( + 'type', 'custom_id', 'placeholder', 'min_values', 'max_values', 'options', 'disabled', + 'channel_types', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ def __init__(self, data: SelectMenuPayload, /) -> None: + self.type: ComponentType = try_enum(ComponentType, data['type']) self.custom_id: str = data['custom_id'] self.placeholder: Optional[str] = data.get('placeholder') self.min_values: int = data.get('min_values', 1) self.max_values: int = data.get('max_values', 1) self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get('options', [])] self.disabled: bool = data.get('disabled', False) - - @property - def type(self) -> Literal[ComponentType.select]: - """:class:`ComponentType`: The type of component.""" - return ComponentType.select + self.channel_types: List[ChannelType] = [try_enum(ChannelType, t) for t in data.get('channel_types', [])] def to_dict(self) -> SelectMenuPayload: payload: SelectMenuPayload = { @@ -280,12 +283,14 @@ class SelectMenu(Component): 'custom_id': self.custom_id, 'min_values': self.min_values, 'max_values': self.max_values, - 'options': [op.to_dict() for op in self.options], 'disabled': self.disabled, } - if self.placeholder: payload['placeholder'] = self.placeholder + if self.options: + payload['options'] = [op.to_dict() for op in self.options] + if self.channel_types: + payload['channel_types'] = [t.value for t in self.channel_types] return payload diff --git a/discord/enums.py b/discord/enums.py index 3465250a1..8aacbc67b 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -576,7 +576,12 @@ class ComponentType(Enum): action_row = 1 button = 2 select = 3 + string_select = 3 text_input = 4 + user_select = 5 + role_select = 6 + mentionable_select = 7 + channel_select = 8 def __int__(self) -> int: return self.value diff --git a/discord/types/components.py b/discord/types/components.py index 697490bd6..f1790ff35 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -28,6 +28,7 @@ from typing import List, Literal, TypedDict, Union from typing_extensions import NotRequired from .emoji import PartialEmoji +from .channel import ChannelType ComponentType = Literal[1, 2, 3, 4] ButtonStyle = Literal[1, 2, 3, 4, 5] @@ -57,16 +58,36 @@ class SelectOption(TypedDict): emoji: NotRequired[PartialEmoji] -class SelectMenu(TypedDict): - type: Literal[3] +class SelectComponent(TypedDict): custom_id: str - options: List[SelectOption] placeholder: NotRequired[str] min_values: NotRequired[int] max_values: NotRequired[int] disabled: NotRequired[bool] +class StringSelectComponent(SelectComponent): + type: Literal[3] + options: NotRequired[List[SelectOption]] + + +class UserSelectComponent(SelectComponent): + type: Literal[5] + + +class RoleSelectComponent(SelectComponent): + type: Literal[6] + + +class MentionableSelectComponent(SelectComponent): + type: Literal[7] + + +class ChannelSelectComponent(SelectComponent): + type: Literal[8] + channel_types: NotRequired[List[ChannelType]] + + class TextInput(TypedDict): type: Literal[4] custom_id: str @@ -79,5 +100,11 @@ class TextInput(TypedDict): max_length: NotRequired[int] +class SelectMenu(SelectComponent): + type: Literal[3, 5, 6, 7, 8] + options: NotRequired[List[SelectOption]] + channel_types: NotRequired[List[ChannelType]] + + ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput] Component = Union[ActionRow, ActionRowChildComponent] diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 42f71898f..cfbaf310e 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -160,8 +160,9 @@ class ButtonMessageComponentInteractionData(_BaseMessageComponentInteractionData class SelectMessageComponentInteractionData(_BaseMessageComponentInteractionData): - component_type: Literal[3] + component_type: Literal[3, 5, 6, 7, 8] values: List[str] + resolved: NotRequired[ResolvedData] MessageComponentInteractionData = Union[ButtonMessageComponentInteractionData, SelectMessageComponentInteractionData] diff --git a/discord/ui/item.py b/discord/ui/item.py index 99b3d33bd..03ea669a7 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -76,7 +76,7 @@ class Item(Generic[V]): def _refresh_component(self, component: Component) -> None: return None - def _refresh_state(self, data: Dict[str, Any]) -> None: + def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None: return None @classmethod diff --git a/discord/ui/modal.py b/discord/ui/modal.py index afa3a8f76..1b71fb9c0 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -163,21 +163,21 @@ class Modal(View): """ _log.error('Ignoring exception in modal %r:', self, exc_info=error) - def _refresh(self, components: Sequence[ModalSubmitComponentInteractionDataPayload]) -> None: + def _refresh(self, interaction: Interaction, components: Sequence[ModalSubmitComponentInteractionDataPayload]) -> None: for component in components: if component['type'] == 1: - self._refresh(component['components']) + self._refresh(interaction, component['components']) else: item = find(lambda i: i.custom_id == component['custom_id'], self._children) # type: ignore if item is None: _log.debug("Modal interaction referencing unknown item custom_id %s. Discarding", component['custom_id']) continue - item._refresh_state(component) # type: ignore + item._refresh_state(interaction, component) # type: ignore async def _scheduled_task(self, interaction: Interaction, components: List[ModalSubmitComponentInteractionDataPayload]): try: self._refresh_timeout() - self._refresh(components) + self._refresh(interaction, components) allow = await self.interaction_check(interaction) if not allow: diff --git a/discord/ui/select.py b/discord/ui/select.py index d49225db6..aa3126b26 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -21,15 +21,14 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ - from __future__ import annotations -from typing import List, Literal, Optional, TYPE_CHECKING, Tuple, TypeVar, Callable, Union, Dict +from typing import List, Literal, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Callable, Union, Dict, overload from contextvars import ContextVar import inspect import os from .item import Item, ItemCallbackType -from ..enums import ComponentType +from ..enums import ChannelType, ComponentType from ..partial_emoji import PartialEmoji from ..emoji import Emoji from ..utils import MISSING @@ -37,52 +36,69 @@ from ..components import ( SelectOption, SelectMenu, ) +from ..app_commands.namespace import Namespace __all__ = ( 'Select', + 'UserSelect', + 'RoleSelect', + 'MentionableSelect', + 'ChannelSelect', 'select', ) if TYPE_CHECKING: - from typing_extensions import Self + from typing_extensions import TypeAlias, Self from .view import View from ..types.components import SelectMenu as SelectMenuPayload - from ..types.interactions import ( - MessageComponentInteractionData, - ) + from ..types.interactions import SelectMessageComponentInteractionData + from ..app_commands import AppCommandChannel, AppCommandThread + from ..member import Member + from ..role import Role + from ..user import User + from ..interactions import Interaction + + ValidSelectType: TypeAlias = Literal[ + ComponentType.string_select, + ComponentType.user_select, + ComponentType.role_select, + ComponentType.channel_select, + ComponentType.mentionable_select, + ] + PossibleValue: TypeAlias = Union[ + str, User, Member, Role, AppCommandChannel, AppCommandThread, Union[Role, Member], Union[Role, User] + ] V = TypeVar('V', bound='View', covariant=True) +BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect') +SelectT = TypeVar('SelectT', bound='Select') +UserSelectT = TypeVar('UserSelectT', bound='UserSelect') +RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect') +ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect') +MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect') +SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[V, BaseSelectT]], BaseSelectT] -selected_values: ContextVar[Dict[str, List[str]]] = ContextVar('selected_values') +selected_values: ContextVar[Dict[str, List[PossibleValue]]] = ContextVar('selected_values') -class Select(Item[V]): - """Represents a UI select menu. +class BaseSelect(Item[V]): + """The base Select model that all other Select models inherit from. - This is usually represented as a drop down menu. + This class inherits from :class:`Item` and implements the common attributes. - In order to get the selected items that the user has chosen, use :attr:`Select.values`. + The following implement this class: - .. versionadded:: 2.0 + - :class:`~discord.ui.Select` + - :class:`~discord.ui.ChannelSelect` + - :class:`~discord.ui.RoleSelect` + - :class:`~discord.ui.MentionableSelect` + - :class:`~discord.ui.UserSelect` - Parameters + .. versionadded:: 2.1 + + Attributes ------------ - custom_id: :class:`str` - The ID of the select menu that gets received during an interaction. - If not given then one is generated for you. - placeholder: Optional[:class:`str`] - The placeholder text that is shown if nothing is selected, if any. - min_values: :class:`int` - The minimum number of items that must be chosen for this select menu. - Defaults to 1 and must be between 0 and 25. - max_values: :class:`int` - The maximum number of items that must be chosen for this select menu. - Defaults to 1 and must be between 1 and 25. - options: List[:class:`discord.SelectOption`] - A list of options that can be selected in this menu. - disabled: :class:`bool` - Whether the select is disabled or not. row: Optional[:class:`int`] The relative row this select menu belongs to. A Discord component can only have 5 rows. By default, items are arranged automatically into those 5 rows. If you'd @@ -91,24 +107,27 @@ class Select(Item[V]): ordering. The row number must be between 0 and 4 (i.e. zero indexed). """ + __slots__ = ('_provided_custom_id', '_underlying', 'row', '_values') + __item_repr_attributes__: Tuple[str, ...] = ( 'placeholder', 'min_values', 'max_values', - 'options', 'disabled', ) def __init__( self, + type: ValidSelectType, *, custom_id: str = MISSING, + row: Optional[int] = None, placeholder: Optional[str] = None, - min_values: int = 1, - max_values: int = 1, - options: List[SelectOption] = MISSING, + min_values: Optional[int] = None, + max_values: Optional[int] = None, disabled: bool = False, - row: Optional[int] = None, + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = MISSING, ) -> None: super().__init__() self._provided_custom_id = custom_id is not MISSING @@ -116,17 +135,24 @@ class Select(Item[V]): if not isinstance(custom_id, str): raise TypeError(f'expected custom_id to be str not {custom_id.__class__.__name__}') - options = [] if options is MISSING else options self._underlying = SelectMenu._raw_construct( + type=type, custom_id=custom_id, placeholder=placeholder, min_values=min_values, max_values=max_values, - options=options, disabled=disabled, + channel_types=[] if channel_types is MISSING else channel_types, + options=[] if options is MISSING else options, ) + self.row = row - self._values: List[str] = [] + self._values: List[PossibleValue] = [] + + @property + def values(self) -> List[PossibleValue]: + values = selected_values.get({}) + return values.get(self.custom_id, self._values) @property def custom_id(self) -> str: @@ -164,13 +190,120 @@ class Select(Item[V]): @property def max_values(self) -> int: - """:class:`int`: The maximum number of items that must be chosen for this select menu.""" + """:class:`int`: The maximum number of items that can be chosen for this select menu.""" return self._underlying.max_values @max_values.setter def max_values(self, value: int) -> None: self._underlying.max_values = int(value) + @property + def disabled(self) -> bool: + """:class:`bool`: Whether the select is disabled or not.""" + return self._underlying.disabled + + @disabled.setter + def disabled(self, value: bool) -> None: + self._underlying.disabled = bool(value) + + @property + def width(self) -> int: + return 5 + + def to_component_dict(self) -> SelectMenuPayload: + return self._underlying.to_dict() + + def _refresh_component(self, component: SelectMenu) -> None: + self._underlying = component + + def _refresh_state(self, interaction: Interaction, data: SelectMessageComponentInteractionData) -> None: + values = selected_values.get({}) + payload: List[PossibleValue] + try: + resolved = Namespace._get_resolved_items(interaction, data['resolved']) + payload = list(resolved.values()) + except KeyError: + payload = data.get("values", []) # type: ignore + + self._values = values[self.custom_id] = payload + selected_values.set(values) + + def is_dispatchable(self) -> bool: + return True + + @classmethod + def from_component(cls, component: SelectMenu) -> Self: + return cls( + **{k: getattr(component, k) for k in cls.__item_repr_attributes__}, + row=None, + ) + + +class Select(BaseSelect[V]): + """Represents a UI select menu with a list of custom options. This is represented + to the user as a dropdown menu. + + .. versionadded:: 2.0 + + Parameters + ------------ + custom_id: :class:`str` + The ID of the select menu that gets received during an interaction. + If not given then one is generated for you. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 0 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + options: List[:class:`discord.SelectOption`] + A list of options that can be selected in this menu. + disabled: :class:`bool` + Whether the select is disabled or not. + row: Optional[:class:`int`] + The relative row this select menu belongs to. A Discord component can only have 5 + rows. By default, items are arranged automatically into those 5 rows. If you'd + like to control the relative positioning of the row then passing an index is advised. + For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 4 (i.e. zero indexed). + """ + + __item_repr_attributes__ = BaseSelect.__item_repr_attributes__ + ('options',) + + def __init__( + self, + *, + custom_id: str = MISSING, + placeholder: Optional[str] = None, + min_values: int = 1, + max_values: int = 1, + options: List[SelectOption] = MISSING, + disabled: bool = False, + row: Optional[int] = None, + ) -> None: + super().__init__( + self.type, + custom_id=custom_id, + placeholder=placeholder, + min_values=min_values, + max_values=max_values, + disabled=disabled, + options=options, + row=row, + ) + + @property + def values(self) -> List[str]: + """List[:class:`str`]: A list of values that have been selected by the user.""" + return super().values # type: ignore + + @property + def type(self) -> Literal[ComponentType.string_select]: + """:class:`.ComponentType`: The type of this component.""" + return ComponentType.string_select + @property def options(self) -> List[SelectOption]: """List[:class:`discord.SelectOption`]: A list of options that can be selected in this menu.""" @@ -251,77 +384,419 @@ class Select(Item[V]): self._underlying.options.append(option) + +class UserSelect(BaseSelect[V]): + """Represents a UI select menu with a list of predefined options with the current members of the guild. + + If this is sent a private message, it will only allow the user to select the client + or themselves. Every selected option in a private message will resolve to + a :class:`discord.User`. + + .. versionadded:: 2.1 + + Parameters + ------------ + custom_id: :class:`str` + The ID of the select menu that gets received during an interaction. + If not given then one is generated for you. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 0 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + disabled: :class:`bool` + Whether the select is disabled or not. + row: Optional[:class:`int`] + The relative row this select menu belongs to. A Discord component can only have 5 + rows. By default, items are arranged automatically into those 5 rows. If you'd + like to control the relative positioning of the row then passing an index is advised. + For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 4 (i.e. zero indexed). + """ + + def __init__( + self, + *, + custom_id: str = MISSING, + placeholder: Optional[str] = None, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + row: Optional[int] = None, + ) -> None: + super().__init__( + self.type, + custom_id=custom_id, + placeholder=placeholder, + min_values=min_values, + max_values=max_values, + disabled=disabled, + row=row, + ) + @property - def disabled(self) -> bool: - """:class:`bool`: Whether the select is disabled or not.""" - return self._underlying.disabled + def type(self) -> Literal[ComponentType.user_select]: + """:class:`.ComponentType`: The type of this component.""" + return ComponentType.user_select - @disabled.setter - def disabled(self, value: bool) -> None: - self._underlying.disabled = bool(value) + @property + def values(self) -> List[Union[Member, User]]: + """List[Union[:class:`discord.Member`, :class:`discord.User`]]: A list of members + and users that have been selected by the user. + + If this is sent a private message, it will only allow + the user to select the client or themselves. Every selected option in a private + message will resolve to a :class:`discord.User`. + + If invoked in a guild, the values will always resolve to :class:`discord.Member`. + """ + return super().values # type: ignore + + +class RoleSelect(BaseSelect[V]): + """Represents a UI select menu with a list of predefined options with the current roles of the guild. + + Please note that if you use this in a private message with a user, no roles will be displayed to the user. + + .. versionadded:: 2.1 + + Parameters + ------------ + custom_id: :class:`str` + The ID of the select menu that gets received during an interaction. + If not given then one is generated for you. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 0 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + disabled: :class:`bool` + Whether the select is disabled or not. + row: Optional[:class:`int`] + The relative row this select menu belongs to. A Discord component can only have 5 + rows. By default, items are arranged automatically into those 5 rows. If you'd + like to control the relative positioning of the row then passing an index is advised. + For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 4 (i.e. zero indexed). + """ + + def __init__( + self, + *, + custom_id: str = MISSING, + placeholder: Optional[str] = None, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + row: Optional[int] = None, + ) -> None: + super().__init__( + self.type, + custom_id=custom_id, + placeholder=placeholder, + min_values=min_values, + max_values=max_values, + disabled=disabled, + row=row, + ) @property - def values(self) -> List[str]: - """List[:class:`str`]: A list of values that have been selected by the user.""" - values = selected_values.get({}) - return values.get(self.custom_id, self._values) + def type(self) -> Literal[ComponentType.role_select]: + """:class:`.ComponentType`: The type of this component.""" + return ComponentType.role_select @property - def width(self) -> int: - return 5 + def values(self) -> List[Role]: + """List[:class:`discord.Role`]: A list of roles that have been selected by the user.""" + return super().values # type: ignore - def to_component_dict(self) -> SelectMenuPayload: - return self._underlying.to_dict() - def _refresh_component(self, component: SelectMenu) -> None: - self._underlying = component +class MentionableSelect(BaseSelect[V]): + """Represents a UI select menu with a list of predefined options with the current members and roles in the guild. - def _refresh_state(self, data: MessageComponentInteractionData) -> None: - values = selected_values.get({}) - self._values = values[self.custom_id] = data.get('values', []) - selected_values.set(values) + If this is sent in a private message, it will only allow the user to select + the client or themselves. Every selected option in a private message + will resolve to a :class:`discord.User`. It will not give the user any roles + to select. - @classmethod - def from_component(cls, component: SelectMenu) -> Self: - return cls( - custom_id=component.custom_id, - placeholder=component.placeholder, - min_values=component.min_values, - max_values=component.max_values, - options=component.options, - disabled=component.disabled, - row=None, + .. versionadded:: 2.1 + + Parameters + ------------ + custom_id: :class:`str` + The ID of the select menu that gets received during an interaction. + If not given then one is generated for you. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 0 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + disabled: :class:`bool` + Whether the select is disabled or not. + row: Optional[:class:`int`] + The relative row this select menu belongs to. A Discord component can only have 5 + rows. By default, items are arranged automatically into those 5 rows. If you'd + like to control the relative positioning of the row then passing an index is advised. + For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 4 (i.e. zero indexed). + """ + + def __init__( + self, + *, + custom_id: str = MISSING, + placeholder: Optional[str] = None, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + row: Optional[int] = None, + ) -> None: + super().__init__( + self.type, + custom_id=custom_id, + placeholder=placeholder, + min_values=min_values, + max_values=max_values, + disabled=disabled, + row=row, ) @property - def type(self) -> Literal[ComponentType.select]: - return self._underlying.type + def type(self) -> Literal[ComponentType.mentionable_select]: + """:class:`.ComponentType`: The type of this component.""" + return ComponentType.mentionable_select - def is_dispatchable(self) -> bool: - return True + @property + def values(self) -> List[Union[Member, User, Role]]: + """List[Union[:class:`discord.Role`, :class:`discord.Member`, :class:`discord.User`]]: A list of roles, members, + and users that have been selected by the user. + + If this is sent a private message, it will only allow + the user to select the client or themselves. Every selected option in a private + message will resolve to a :class:`discord.User`. + + If invoked in a guild, the values will always resolve to :class:`discord.Member`. + """ + return super().values # type: ignore + + +class ChannelSelect(BaseSelect[V]): + """Represents a UI select menu with a list of predefined options with the current channels in the guild. + + Please note that if you use this in a private message with a user, no channels will be displayed to the user. + + .. versionadded:: 2.1 + + Parameters + ------------ + custom_id: :class:`str` + The ID of the select menu that gets received during an interaction. + If not given then one is generated for you. + channel_types: List[:class:`~discord.ChannelType`] + The types of channels to show in the select menu. Defaults to all channels. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 0 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + disabled: :class:`bool` + Whether the select is disabled or not. + row: Optional[:class:`int`] + The relative row this select menu belongs to. A Discord component can only have 5 + rows. By default, items are arranged automatically into those 5 rows. If you'd + like to control the relative positioning of the row then passing an index is advised. + For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 4 (i.e. zero indexed). + """ + __item_repr_attributes__ = BaseSelect.__item_repr_attributes__ + ('channel_types',) + def __init__( + self, + *, + custom_id: str = MISSING, + channel_types: List[ChannelType] = MISSING, + placeholder: Optional[str] = None, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + row: Optional[int] = None, + ) -> None: + super().__init__( + self.type, + custom_id=custom_id, + placeholder=placeholder, + min_values=min_values, + max_values=max_values, + disabled=disabled, + row=row, + channel_types=channel_types, + ) + + @property + def type(self) -> Literal[ComponentType.channel_select]: + """:class:`.ComponentType`: The type of this component.""" + return ComponentType.channel_select + + @property + def channel_types(self) -> List[ChannelType]: + """List[:class:`~discord.ChannelType`]: A list of channel types that can be selected.""" + return self._underlying.channel_types + + @property + def values(self) -> List[Union[AppCommandChannel, AppCommandThread]]: + """List[Union[:class:`~discord.app_commands.AppCommandChannel`, :class:`~discord.app_commands.AppCommandThread`]]: A list of channels selected by the user.""" + return super().values # type: ignore + + +@overload def select( *, + cls: Type[SelectT] = Select, + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + row: Optional[int] = ..., +) -> SelectCallbackDecorator[V, SelectT]: + ... + + +@overload +def select( + *, + cls: Type[UserSelectT], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + row: Optional[int] = ..., +) -> SelectCallbackDecorator[V, UserSelectT]: + ... + + +@overload +def select( + *, + cls: Type[RoleSelectT], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + row: Optional[int] = ..., +) -> SelectCallbackDecorator[V, RoleSelectT]: + ... + + +@overload +def select( + *, + cls: Type[ChannelSelectT], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + row: Optional[int] = ..., +) -> SelectCallbackDecorator[V, ChannelSelectT]: + ... + + +@overload +def select( + *, + cls: Type[MentionableSelectT], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = MISSING, + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + row: Optional[int] = ..., +) -> SelectCallbackDecorator[V, MentionableSelectT]: + ... + + +def select( + *, + cls: Type[BaseSelectT] = Select, + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = MISSING, placeholder: Optional[str] = None, custom_id: str = MISSING, min_values: int = 1, max_values: int = 1, - options: List[SelectOption] = MISSING, disabled: bool = False, row: Optional[int] = None, -) -> Callable[[ItemCallbackType[V, Select[V]]], Select[V]]: +) -> SelectCallbackDecorator[V, BaseSelectT]: """A decorator that attaches a select menu to a component. The function being decorated should have three parameters, ``self`` representing the :class:`discord.ui.View`, the :class:`discord.Interaction` you receive and - the :class:`discord.ui.Select` being used. - - In order to get the selected items that the user has chosen within the callback - use :attr:`Select.values`. + the chosen select class. + + To obtain the selected values inside the callback, you can use the ``values`` attribute of the chosen class in the callback. The list of values + will depend on the type of select menu used. View the table below for more information. + + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | Select Type | Resolved Values | + +========================================+=================================================================================================================+ + | :class:`discord.ui.Select` | List[:class:`str`] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.UserSelect` | List[Union[:class:`discord.Member`, :class:`discord.User`]] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.RoleSelect` | List[:class:`discord.Role`] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.MentionableSelect` | List[Union[:class:`discord.Role`, :class:`discord.Member`, :class:`discord.User`]] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.ChannelSelect` | List[Union[:class:`~discord.app_commands.AppCommandChannel`, :class:`~discord.app_commands.AppCommandThread`]] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + + .. versionchanged:: 2.1 + Added the following keyword-arguments: ``cls``, ``channel_types`` + + Example + --------- + .. code-block:: python3 + + class View(discord.ui.View): + + @discord.ui.select(cls=ChannelSelect, channel_types=[discord.ChannelType.text]) + async def select_channels(self, interaction: discord.Interaction, select: ChannelSelect): + return await interaction.response.send_message(f'You selected {select.values[0].mention}') Parameters ------------ + cls: Union[Type[:class:`discord.ui.Select`], Type[:class:`discord.ui.UserSelect`], Type[:class:`discord.ui.RoleSelect`], \ + Type[:class:`discord.ui.MentionableSelect`], Type[:class:`discord.ui.ChannelSelect`]] + The class to use for the select menu. Defaults to :class:`discord.ui.Select`. You can use other + select types to display different select menus to the user. See the table above for the different + values you can get from each select type. Subclasses work as well, however the callback in the subclass will + get overridden. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. custom_id: :class:`str` @@ -340,25 +815,36 @@ def select( The maximum number of items that must be chosen for this select menu. Defaults to 1 and must be between 1 and 25. options: List[:class:`discord.SelectOption`] - A list of options that can be selected in this menu. + A list of options that can be selected in this menu. This can only be used with + :class:`Select` instances. + channel_types: List[:class:`~discord.ChannelType`] + The types of channels to show in the select menu. Defaults to all channels. This can only be used + with :class:`ChannelSelect` instances. disabled: :class:`bool` Whether the select is disabled or not. Defaults to ``False``. """ - def decorator(func: ItemCallbackType[V, Select[V]]) -> ItemCallbackType[V, Select[V]]: + def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]: if not inspect.iscoroutinefunction(func): raise TypeError('select function must be a coroutine function') + if not issubclass(cls, BaseSelect): + supported_classes = ", ".join(["ChannelSelect", "MentionableSelect", "RoleSelect", "Select", "UserSelect"]) + raise TypeError(f'cls must be one of {supported_classes} or a subclass of one of them, not {cls!r}.') - func.__discord_ui_model_type__ = Select + func.__discord_ui_model_type__ = cls func.__discord_ui_model_kwargs__ = { 'placeholder': placeholder, 'custom_id': custom_id, 'row': row, 'min_values': min_values, 'max_values': max_values, - 'options': options, 'disabled': disabled, } + if issubclass(cls, Select): + func.__discord_ui_model_kwargs__['options'] = options + if issubclass(cls, ChannelSelect): + func.__discord_ui_model_kwargs__['channel_types'] = channel_types + return func return decorator # type: ignore diff --git a/discord/ui/text_input.py b/discord/ui/text_input.py index 1ea84c6e5..79ac652b9 100644 --- a/discord/ui/text_input.py +++ b/discord/ui/text_input.py @@ -38,6 +38,7 @@ if TYPE_CHECKING: from ..types.components import TextInput as TextInputPayload from ..types.interactions import ModalSubmitTextInputInteractionData as ModalSubmitTextInputInteractionDataPayload from .view import View + from ..interactions import Interaction # fmt: off @@ -218,7 +219,7 @@ class TextInput(Item[V]): def _refresh_component(self, component: TextInputComponent) -> None: self._underlying = component - def _refresh_state(self, data: ModalSubmitTextInputInteractionDataPayload) -> None: + def _refresh_state(self, interaction: Interaction, data: ModalSubmitTextInputInteractionDataPayload) -> None: self._value = data.get('value', None) @classmethod diff --git a/discord/ui/view.py b/discord/ui/view.py index ab02ed7d0..6c37fe7a9 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -413,7 +413,7 @@ class View: async def _scheduled_task(self, item: Item, interaction: Interaction): try: - item._refresh_state(interaction.data) # type: ignore + item._refresh_state(interaction, interaction.data) # type: ignore allow = await self.interaction_check(interaction) if not allow: diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index edcebb1d2..107d88602 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -252,16 +252,34 @@ Enumerations .. attribute:: action_row Represents the group component which holds different components in a row. + .. attribute:: button Represents a button component. + + .. attribute:: text_input + + Represents a text box component. + .. attribute:: select Represents a select component. - .. attribute:: text_input + .. attribute:: string_select - Represents a text box component. + An alias to :attr:`select`. Represents a default select component. + + .. attribute:: user_select + + Represents a user select component. + + .. attribute:: role_select + + Represents a role select component. + + .. attribute:: mentionable_select + + Represents a select in which both users and roles can be selected. .. class:: ButtonStyle @@ -437,8 +455,13 @@ Button .. autofunction:: discord.ui.button :decorator: +Select Menus +~~~~~~~~~~~~~ + +The library provides classes to help create the different types of select menus. + Select -~~~~~~~ ++++++++ .. attributetable:: discord.ui.Select @@ -446,11 +469,50 @@ Select :members: :inherited-members: +ChannelSelect +++++++++++++++ + +.. attributetable:: discord.ui.ChannelSelect + +.. autoclass:: discord.ui.ChannelSelect + :members: + :inherited-members: + +RoleSelect +++++++++++ + +.. attributetable:: discord.ui.RoleSelect + +.. autoclass:: discord.ui.RoleSelect + :members: + :inherited-members: + +MentionableSelect +++++++++++++++++++ + +.. attributetable:: discord.ui.MentionableSelect + +.. autoclass:: discord.ui.MentionableSelect + :members: + :inherited-members: + +UserSelect ++++++++++++ + +.. attributetable:: discord.ui.UserSelect + +.. autoclass:: discord.ui.UserSelect + :members: + :inherited-members: + +select ++++++++ .. autofunction:: discord.ui.select :decorator: + TextInput -~~~~~~~~~~ +~~~~~~~~~~~ .. attributetable:: discord.ui.TextInput