358 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			358 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""
 | 
						|
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 ..emoji import Emoji
 | 
						|
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.
 | 
						|
 | 
						|
    In order to get the selected items that the user has chosen, use :attr:`Select.values`.
 | 
						|
 | 
						|
    .. 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.
 | 
						|
    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__: Tuple[str, ...] = (
 | 
						|
        'placeholder',
 | 
						|
        'min_values',
 | 
						|
        'max_values',
 | 
						|
        'options',
 | 
						|
        'disabled',
 | 
						|
    )
 | 
						|
 | 
						|
    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._selected_values: List[str] = []
 | 
						|
        self._provided_custom_id = custom_id is not MISSING
 | 
						|
        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,
 | 
						|
            disabled=disabled,
 | 
						|
        )
 | 
						|
        self.row = row
 | 
						|
 | 
						|
    @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
 | 
						|
 | 
						|
    @options.setter
 | 
						|
    def options(self, value: List[SelectOption]):
 | 
						|
        if not isinstance(value, list):
 | 
						|
            raise TypeError('options must be a list of SelectOption')
 | 
						|
        if not all(isinstance(obj, SelectOption) for obj in value):
 | 
						|
            raise TypeError('all list items must subclass SelectOption')
 | 
						|
 | 
						|
        self._underlying.options = value
 | 
						|
 | 
						|
    def add_option(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        label: str,
 | 
						|
        value: str = MISSING,
 | 
						|
        description: Optional[str] = None,
 | 
						|
        emoji: Optional[Union[str, Emoji, 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 100 characters.
 | 
						|
        value: :class:`str`
 | 
						|
            The value of the option. This is not displayed to users.
 | 
						|
            If not given, defaults to the label. Can only be up to 100 characters.
 | 
						|
        description: Optional[:class:`str`]
 | 
						|
            An additional description of the option, if any.
 | 
						|
            Can only be up to 100 characters.
 | 
						|
        emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :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` or :class:`.Emoji`.
 | 
						|
        default: :class:`bool`
 | 
						|
            Whether this option is selected by default.
 | 
						|
 | 
						|
        Raises
 | 
						|
        -------
 | 
						|
        ValueError
 | 
						|
            The number of options exceeds 25.
 | 
						|
        """
 | 
						|
 | 
						|
        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 disabled(self) -> bool:
 | 
						|
        """:class:`bool`: Whether the select is disabled or not."""
 | 
						|
        return self._underlying.disabled
 | 
						|
 | 
						|
    @disabled.setter
 | 
						|
    def disabled(self, value: bool):
 | 
						|
        self._underlying.disabled = bool(value)
 | 
						|
 | 
						|
    @property
 | 
						|
    def values(self) -> List[str]:
 | 
						|
        """List[:class:`str`]: A list of values that have been selected by the user."""
 | 
						|
        return self._selected_values
 | 
						|
 | 
						|
    @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) -> 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,
 | 
						|
            disabled=component.disabled,
 | 
						|
            row=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,
 | 
						|
    disabled: bool = False,
 | 
						|
    row: 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.
 | 
						|
 | 
						|
    In order to get the selected items that the user has chosen within the callback
 | 
						|
    use :attr:`Select.values`.
 | 
						|
 | 
						|
    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.
 | 
						|
    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).
 | 
						|
    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.
 | 
						|
    disabled: :class:`bool`
 | 
						|
        Whether the select is disabled or not. Defaults to ``False``.
 | 
						|
    """
 | 
						|
 | 
						|
    def decorator(func: ItemCallbackType) -> ItemCallbackType:
 | 
						|
        if not inspect.iscoroutinefunction(func):
 | 
						|
            raise TypeError('select function must be a coroutine function')
 | 
						|
 | 
						|
        func.__discord_ui_model_type__ = Select
 | 
						|
        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,
 | 
						|
        }
 | 
						|
        return func
 | 
						|
 | 
						|
    return decorator
 |