mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-05-16 18:59:09 +00:00
Add support for select components
This commit is contained in:
parent
6874aa73c4
commit
ef9f61a933
@ -515,7 +515,7 @@ class ConnectionState:
|
|||||||
self.dispatch('raw_message_edit', raw)
|
self.dispatch('raw_message_edit', raw)
|
||||||
|
|
||||||
if 'components' in data and self._view_store.is_message_tracked(raw.message_id):
|
if 'components' in data and self._view_store.is_message_tracked(raw.message_id):
|
||||||
self._view_store.update_view(raw.message_id, data['components'])
|
self._view_store.update_from_message(raw.message_id, data['components'])
|
||||||
|
|
||||||
def parse_message_reaction_add(self, data):
|
def parse_message_reaction_add(self, data):
|
||||||
emoji = data['emoji']
|
emoji = data['emoji']
|
||||||
|
@ -124,7 +124,11 @@ class ApplicationCommandInteractionData(_ApplicationCommandInteractionDataOption
|
|||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|
||||||
class ComponentInteractionData(TypedDict):
|
class _ComponentInteractionDataOptional(TypedDict, total=False):
|
||||||
|
values: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentInteractionData(_ComponentInteractionDataOptional):
|
||||||
custom_id: str
|
custom_id: str
|
||||||
component_type: ComponentType
|
component_type: ComponentType
|
||||||
|
|
||||||
@ -154,7 +158,7 @@ class InteractionApplicationCommandCallbackData(TypedDict, total=False):
|
|||||||
flags: int
|
flags: int
|
||||||
|
|
||||||
|
|
||||||
InteractionResponseType = Literal[1, 2, 3, 4, 5]
|
InteractionResponseType = Literal[1, 2, 3, 4, 5, 6, 7]
|
||||||
|
|
||||||
|
|
||||||
class _InteractionResponseOptional(TypedDict, total=False):
|
class _InteractionResponseOptional(TypedDict, total=False):
|
||||||
|
@ -12,3 +12,4 @@ Bot UI Kit helper for the Discord API
|
|||||||
from .view import *
|
from .view import *
|
||||||
from .item import *
|
from .item import *
|
||||||
from .button import *
|
from .button import *
|
||||||
|
from .select import *
|
||||||
|
@ -202,7 +202,7 @@ class Button(Item[V]):
|
|||||||
def is_dispatchable(self) -> bool:
|
def is_dispatchable(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def refresh_state(self, button: ButtonComponent) -> None:
|
def refresh_component(self, button: ButtonComponent) -> None:
|
||||||
self._underlying = button
|
self._underlying = button
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,7 +59,10 @@ class Item(Generic[V]):
|
|||||||
def to_component_dict(self) -> Dict[str, Any]:
|
def to_component_dict(self) -> Dict[str, Any]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def refresh_state(self, component: Component) -> None:
|
def refresh_component(self, component: Component) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def refresh_state(self, interaction: Interaction) -> None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
315
discord/ui/select.py
Normal file
315
discord/ui/select.py
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
"""
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-present Rapptz
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import List, Optional, TYPE_CHECKING, Tuple, TypeVar, Type, Callable, Union
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .item import Item, ItemCallbackType
|
||||||
|
from ..enums import ComponentType
|
||||||
|
from ..partial_emoji import PartialEmoji
|
||||||
|
from ..interactions import Interaction
|
||||||
|
from ..utils import MISSING
|
||||||
|
from ..components import (
|
||||||
|
SelectOption,
|
||||||
|
SelectMenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Select',
|
||||||
|
'select',
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .view import View
|
||||||
|
from ..types.components import SelectMenu as SelectMenuPayload
|
||||||
|
from ..types.interactions import (
|
||||||
|
ComponentInteractionData,
|
||||||
|
)
|
||||||
|
|
||||||
|
S = TypeVar('S', bound='Select')
|
||||||
|
V = TypeVar('V', bound='View', covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Select(Item[V]):
|
||||||
|
"""Represents a UI select menu.
|
||||||
|
|
||||||
|
This is usually represented as a drop down 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 1 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__item_repr_attributes__: Tuple[str, ...] = (
|
||||||
|
'placeholder',
|
||||||
|
'min_values',
|
||||||
|
'max_values',
|
||||||
|
'options',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
custom_id: str = MISSING,
|
||||||
|
placeholder: Optional[str] = None,
|
||||||
|
min_values: int = 1,
|
||||||
|
max_values: int = 1,
|
||||||
|
options: List[SelectOption] = MISSING,
|
||||||
|
group: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
self._selected_values: List[str] = []
|
||||||
|
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
|
||||||
|
options = [] if options is MISSING else options
|
||||||
|
self._underlying = SelectMenu._raw_construct(
|
||||||
|
custom_id=custom_id,
|
||||||
|
type=ComponentType.select,
|
||||||
|
placeholder=placeholder,
|
||||||
|
min_values=min_values,
|
||||||
|
max_values=max_values,
|
||||||
|
options=options,
|
||||||
|
)
|
||||||
|
self.group_id = group
|
||||||
|
|
||||||
|
@property
|
||||||
|
def custom_id(self) -> str:
|
||||||
|
""":class:`str`: The ID of the select menu that gets received during an interaction."""
|
||||||
|
return self._underlying.custom_id
|
||||||
|
|
||||||
|
@custom_id.setter
|
||||||
|
def custom_id(self, value: str):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError('custom_id must be None or str')
|
||||||
|
|
||||||
|
self._underlying.custom_id = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def placeholder(self) -> Optional[str]:
|
||||||
|
"""Optional[:class:`str`]: The placeholder text that is shown if nothing is selected, if any."""
|
||||||
|
return self._underlying.placeholder
|
||||||
|
|
||||||
|
@placeholder.setter
|
||||||
|
def placeholder(self, value: Optional[str]):
|
||||||
|
if value is not None and not isinstance(value, str):
|
||||||
|
raise TypeError('placeholder must be None or str')
|
||||||
|
|
||||||
|
self._underlying.placeholder = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_values(self) -> int:
|
||||||
|
""":class:`int`: The minimum number of items that must be chosen for this select menu."""
|
||||||
|
return self._underlying.min_values
|
||||||
|
|
||||||
|
@min_values.setter
|
||||||
|
def min_values(self, value: int):
|
||||||
|
self._underlying.min_values = int(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_values(self) -> int:
|
||||||
|
""":class:`int`: The maximum number of items that must be chosen for this select menu."""
|
||||||
|
return self._underlying.max_values
|
||||||
|
|
||||||
|
@max_values.setter
|
||||||
|
def max_values(self, value: int):
|
||||||
|
self._underlying.max_values = int(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> List[SelectOption]:
|
||||||
|
"""List[:class:`discord.SelectOption`]: A list of options that can be selected in this menu."""
|
||||||
|
return self._underlying.options
|
||||||
|
|
||||||
|
def add_option(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
label: str,
|
||||||
|
value: str,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
emoji: Optional[Union[str, PartialEmoji]] = None,
|
||||||
|
default: bool = False,
|
||||||
|
):
|
||||||
|
"""Adds an option to the select menu.
|
||||||
|
|
||||||
|
To append a pre-existing :class:`discord.SelectOption` use the
|
||||||
|
:meth:`append_option` method instead.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
label: :class:`str`
|
||||||
|
The label of the option. This is displayed to users.
|
||||||
|
Can only be up to 25 characters.
|
||||||
|
value: :class:`str`
|
||||||
|
The value of the option. This is not displayed to users.
|
||||||
|
Can only be up to 100 characters.
|
||||||
|
description: Optional[:class:`str`]
|
||||||
|
An additional description of the option, if any.
|
||||||
|
Can only be up to 50 characters.
|
||||||
|
emoji: Optional[Union[:class:`str`, :class:`PartialEmoji`]]
|
||||||
|
The emoji of the option, if available. This can either be a string representing
|
||||||
|
the custom or unicode emoji or an instance of :class:`PartialEmoji`.
|
||||||
|
default: :class:`bool`
|
||||||
|
Whether this option is selected by default.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ValueError
|
||||||
|
The number of options exceeds 25.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(emoji, str):
|
||||||
|
emoji = PartialEmoji.from_str(emoji)
|
||||||
|
|
||||||
|
option = SelectOption(
|
||||||
|
label=label,
|
||||||
|
value=value,
|
||||||
|
description=description,
|
||||||
|
emoji=emoji,
|
||||||
|
default=default,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
self.append_option(option)
|
||||||
|
|
||||||
|
def append_option(self, option: SelectOption):
|
||||||
|
"""Appends an option to the select menu.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
option: :class:`discord.SelectOption`
|
||||||
|
The option to append to the select menu.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ValueError
|
||||||
|
The number of options exceeds 25.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(self._underlying.options) > 25:
|
||||||
|
raise ValueError('maximum number of options already provided')
|
||||||
|
|
||||||
|
self._underlying.options.append(option)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self) -> List[str]:
|
||||||
|
"""List[:class:`str`]: A list of values that have been selected by the user."""
|
||||||
|
return self._selected_values
|
||||||
|
|
||||||
|
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) -> None:
|
||||||
|
data: ComponentInteractionData = interaction.data # type: ignore
|
||||||
|
self._selected_values = data.get('values', [])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_component(cls: Type[S], component: SelectMenu) -> S:
|
||||||
|
return cls(
|
||||||
|
custom_id=component.custom_id,
|
||||||
|
placeholder=component.placeholder,
|
||||||
|
min_values=component.min_values,
|
||||||
|
max_values=component.max_values,
|
||||||
|
options=component.options,
|
||||||
|
group=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> ComponentType:
|
||||||
|
return self._underlying.type
|
||||||
|
|
||||||
|
def is_dispatchable(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def select(
|
||||||
|
*,
|
||||||
|
placeholder: Optional[str] = None,
|
||||||
|
custom_id: str = MISSING,
|
||||||
|
min_values: int = 1,
|
||||||
|
max_values: int = 1,
|
||||||
|
options: List[SelectOption] = MISSING,
|
||||||
|
group: Optional[int] = None,
|
||||||
|
) -> Callable[[ItemCallbackType], ItemCallbackType]:
|
||||||
|
"""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.ui.Select` being pressed and
|
||||||
|
the :class:`discord.Interaction` you receive.
|
||||||
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
placeholder: Optional[:class:`str`]
|
||||||
|
The placeholder text that is shown if nothing is selected, if any.
|
||||||
|
custom_id: :class:`str`
|
||||||
|
The ID of the select menu that gets received during an interaction.
|
||||||
|
It is recommended not to set this parameter to prevent conflicts.
|
||||||
|
group: Optional[:class:`int`]
|
||||||
|
The relative group this select menu belongs to. A Discord component can only have 5
|
||||||
|
groups. By default, items are arranged automatically into those 5 groups. If you'd
|
||||||
|
like to control the relative positioning of the group then passing an index is advised.
|
||||||
|
For example, group=1 will show up before group=2. Defaults to ``None``, which is automatic
|
||||||
|
ordering.
|
||||||
|
min_values: :class:`int`
|
||||||
|
The minimum number of items that must be chosen for this select menu.
|
||||||
|
Defaults to 1 and must be between 1 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: ItemCallbackType) -> ItemCallbackType:
|
||||||
|
if not inspect.iscoroutinefunction(func):
|
||||||
|
raise TypeError('button function must be a coroutine function')
|
||||||
|
|
||||||
|
func.__discord_ui_model_type__ = Select
|
||||||
|
func.__discord_ui_model_kwargs__ = {
|
||||||
|
'placeholder': placeholder,
|
||||||
|
'custom_id': custom_id,
|
||||||
|
'group': group,
|
||||||
|
'min_values': min_values,
|
||||||
|
'max_values': max_values,
|
||||||
|
'options': options,
|
||||||
|
}
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
@ -35,6 +35,7 @@ from .item import Item, ItemCallbackType
|
|||||||
from ..enums import ComponentType
|
from ..enums import ComponentType
|
||||||
from ..components import (
|
from ..components import (
|
||||||
Component,
|
Component,
|
||||||
|
ActionRow as ActionRowComponent,
|
||||||
_component_factory,
|
_component_factory,
|
||||||
Button as ButtonComponent,
|
Button as ButtonComponent,
|
||||||
)
|
)
|
||||||
@ -52,7 +53,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
def _walk_all_components(components: List[Component]) -> Iterator[Component]:
|
def _walk_all_components(components: List[Component]) -> Iterator[Component]:
|
||||||
for item in components:
|
for item in components:
|
||||||
if item.type is ComponentType.action_row:
|
if isinstance(item, ActionRowComponent):
|
||||||
yield from item.children
|
yield from item.children
|
||||||
else:
|
else:
|
||||||
yield item
|
yield item
|
||||||
@ -115,6 +116,7 @@ class View:
|
|||||||
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
|
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
|
||||||
item.callback = partial(func, self, item)
|
item.callback = partial(func, self, item)
|
||||||
item._view = self
|
item._view = self
|
||||||
|
setattr(self, func.__name__, item)
|
||||||
self.children.append(item)
|
self.children.append(item)
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
@ -277,7 +279,7 @@ class View:
|
|||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
children.append(_component_to_item(component))
|
children.append(_component_to_item(component))
|
||||||
else:
|
else:
|
||||||
older.refresh_state(component)
|
older.refresh_component(component)
|
||||||
children.append(older)
|
children.append(older)
|
||||||
|
|
||||||
self.children = children
|
self.children = children
|
||||||
@ -358,12 +360,13 @@ class ViewStore:
|
|||||||
|
|
||||||
view, item, _ = value
|
view, item, _ = value
|
||||||
self._views[key] = (view, item, view._expires_at)
|
self._views[key] = (view, item, view._expires_at)
|
||||||
|
item.refresh_state(interaction)
|
||||||
view.dispatch(self._state, item, interaction)
|
view.dispatch(self._state, item, interaction)
|
||||||
|
|
||||||
def is_message_tracked(self, message_id: int):
|
def is_message_tracked(self, message_id: int):
|
||||||
return message_id in self._synced_message_views
|
return message_id in self._synced_message_views
|
||||||
|
|
||||||
def update_view(self, message_id: int, components: List[ComponentPayload]):
|
def update_from_message(self, message_id: int, components: List[ComponentPayload]):
|
||||||
# pre-req: is_message_tracked == true
|
# pre-req: is_message_tracked == true
|
||||||
view = self._synced_message_views[message_id]
|
view = self._synced_message_views[message_id]
|
||||||
view.refresh([_component_factory(d) for d in components])
|
view.refresh([_component_factory(d) for d in components])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user