Source code for lightbulb.components.menus

# -*- coding: utf-8 -*-
#
# api_ref_gen::add_autodoc_option::inherited-members
#
# Copyright (c) 2023-present tandemdude
#
# 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

__all__ = [
    "ChannelSelect",
    "InteractiveButton",
    "LinkButton",
    "MentionableSelect",
    "Menu",
    "MenuContext",
    "MenuHandle",
    "RoleSelect",
    "Select",
    "TextSelect",
    "TextSelectOption",
    "UserSelect",
]

import abc
import asyncio
import contextlib
import contextvars
import typing as t
import uuid

import async_timeout
import hikari
import linkd
from hikari.api import special_endpoints
from hikari.impl import special_endpoints as special_endpoints_impl

from lightbulb.components import base

if t.TYPE_CHECKING:
    from collections.abc import Awaitable
    from collections.abc import Callable
    from collections.abc import Sequence

    import typing_extensions as t_ex

    from lightbulb import client as client_

    ValidSelectOptions: t.TypeAlias = t.Union[Sequence["TextSelectOption"], Sequence[str], Sequence[tuple[str, str]]]
    ComponentCallback: t.TypeAlias = Callable[["MenuContext"], Awaitable[None]]

T = t.TypeVar("T")
MessageComponentT = t.TypeVar("MessageComponentT", bound=base.BaseComponent[special_endpoints.MessageActionRowBuilder])

Emojiish: t.TypeAlias = t.Union[hikari.Snowflakeish, str, hikari.Emoji]


[docs] class InteractiveButton(base.BaseComponent[special_endpoints.MessageActionRowBuilder]): """Class representing an interactive button.""" __slots__ = ("_custom_id", "callback", "disabled", "emoji", "label", "style") def __init__( self, style: hikari.ButtonStyle, custom_id: str, label: hikari.UndefinedOr[str], emoji: hikari.UndefinedOr[Emojiish], disabled: bool, callback: ComponentCallback, ) -> None: self.style: hikari.ButtonStyle = style """The style of the button.""" self._custom_id: str = custom_id self.label: hikari.UndefinedOr[str] = label """The label for the button.""" self.emoji: hikari.UndefinedOr[Emojiish] = emoji """The emoji for the button.""" self.disabled: bool = disabled """Whether the button is disabled.""" self.callback: ComponentCallback = callback """The callback method to call when the button is pressed.""" @property def custom_id(self) -> str: """The custom id of the button.""" return self._custom_id
[docs] def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_endpoints.MessageActionRowBuilder: return row.add_interactive_button( self.style, # type: ignore[reportArgumentType] self.custom_id, emoji=self.emoji, label=self.label, is_disabled=self.disabled, )
[docs] class LinkButton(base.BaseComponent[special_endpoints.MessageActionRowBuilder]): """Dataclass representing a link button.""" __slots__ = ("disabled", "emoji", "label", "url") def __init__( self, url: str, label: hikari.UndefinedOr[str], emoji: hikari.UndefinedOr[Emojiish], disabled: bool ) -> None: self.url: str = url """The url the button links to.""" self.label: hikari.UndefinedOr[str] = label """The label for the button.""" self.emoji: hikari.UndefinedOr[Emojiish] = emoji """The emoji for the button.""" self.disabled: bool = disabled """Whether the button is disabled.""" @property def custom_id(self) -> str: return "__lightbulb_placeholder__"
[docs] def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_endpoints.MessageActionRowBuilder: return row.add_link_button( self.url, emoji=self.emoji, label=self.label, is_disabled=self.disabled, )
[docs] class Select(t.Generic[T], base.BaseComponent[special_endpoints.MessageActionRowBuilder], abc.ABC): """Dataclass representing a generic select menu.""" __slots__ = ("_custom_id", "callback", "disabled", "max_values", "min_values", "placeholder") def __init__( self, custom_id: str, placeholder: hikari.UndefinedOr[str], min_values: int, max_values: int, disabled: bool, callback: ComponentCallback, ) -> None: self._custom_id: str = custom_id self.placeholder: hikari.UndefinedOr[str] = placeholder """The placeholder for the select menu.""" self.min_values: int = min_values """The minimum number of items that can be selected.""" self.max_values: int = max_values """The maximum number of items that can be selected.""" self.disabled: bool = disabled """Whether the select menu is disabled.""" self.callback: ComponentCallback = callback """The callback method to call when the select menu is submitted.""" @property def custom_id(self) -> str: """The custom id of the select menu.""" return self._custom_id
[docs] class TextSelectOption: """Class representing an option for a text select menu.""" __slots__ = ("default", "description", "emoji", "label", "value") def __init__( self, label: str, value: str, description: hikari.UndefinedOr[str] = hikari.UNDEFINED, emoji: hikari.UndefinedOr[Emojiish] = hikari.UNDEFINED, default: bool = False, ) -> None: self.label: str = label """The label for the option.""" self.value: str = value """The value of the option.""" self.description: hikari.UndefinedOr[str] = description """The description of the option.""" self.emoji: hikari.UndefinedOr[Emojiish] = emoji """The emoji for the option.""" self.default: bool = default """Whether this option should be set as selected by default."""
[docs] class TextSelect(Select[str]): """Class representing a select menu with text options.""" __slots__ = ("options",) def __init__( self, custom_id: str, placeholder: hikari.UndefinedOr[str], min_values: int, max_values: int, disabled: bool, callback: ComponentCallback, options: ValidSelectOptions, ) -> None: super().__init__(custom_id, placeholder, min_values, max_values, disabled, callback) self.options: ValidSelectOptions = options """The options for the select menu."""
[docs] def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_endpoints.MessageActionRowBuilder: normalised_options: list[TextSelectOption] = [] for option in self.options: if isinstance(option, str): normalised_options.append(TextSelectOption(option, option)) elif isinstance(option, tuple): normalised_options.append(TextSelectOption(option[0], option[1])) else: normalised_options.append(option) bld = row.add_text_menu( self.custom_id, placeholder=self.placeholder, min_values=self.min_values, max_values=self.max_values, is_disabled=self.disabled, ) for opt in normalised_options: bld = bld.add_option( opt.label, opt.value, description=opt.description, emoji=opt.emoji, is_default=opt.default ) return bld.parent
[docs] class UserSelect(Select[hikari.User]): """Class representing a select menu with user options.""" __slots__ = ()
[docs] def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_endpoints.MessageActionRowBuilder: return row.add_select_menu( hikari.ComponentType.USER_SELECT_MENU, self.custom_id, placeholder=self.placeholder, min_values=self.min_values, max_values=self.max_values, is_disabled=self.disabled, )
[docs] class RoleSelect(Select[hikari.Role]): """Class representing a select menu with role options.""" __slots__ = ()
[docs] def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_endpoints.MessageActionRowBuilder: return row.add_select_menu( hikari.ComponentType.ROLE_SELECT_MENU, self.custom_id, placeholder=self.placeholder, min_values=self.min_values, max_values=self.max_values, is_disabled=self.disabled, )
[docs] class MentionableSelect(Select[hikari.Unique]): """Class representing a select menu with snowflake options.""" __slots__ = ()
[docs] def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_endpoints.MessageActionRowBuilder: return row.add_select_menu( hikari.ComponentType.MENTIONABLE_SELECT_MENU, self.custom_id, placeholder=self.placeholder, min_values=self.min_values, max_values=self.max_values, is_disabled=self.disabled, )
[docs] class ChannelSelect(Select[hikari.PartialChannel]): """Class representing a select menu with channel options.""" __slots__ = ("channel_types",) def __init__( self, custom_id: str, placeholder: hikari.UndefinedOr[str], min_values: int, max_values: int, disabled: bool, callback: ComponentCallback, channel_types: hikari.UndefinedOr[Sequence[hikari.ChannelType]], ) -> None: super().__init__(custom_id, placeholder, min_values, max_values, disabled, callback) self.channel_types: hikari.UndefinedOr[Sequence[hikari.ChannelType]] = channel_types """Channel types permitted to be shown as options."""
[docs] def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_endpoints.MessageActionRowBuilder: return row.add_channel_menu( self.custom_id, channel_types=self.channel_types or (), placeholder=self.placeholder, min_values=self.min_values, max_values=self.max_values, is_disabled=self.disabled, )
class _MenuInteractionHandlerContainer: __slots__ = ("_client", "_ctx", "_menu", "_stop_event", "_tm", "custom_ids") def __init__( self, client: client_.Client, menu: Menu, tm: async_timeout.Timeout | None, stop_event: asyncio.Event, ctx: contextvars.Context | None, ) -> None: self._client = client self._menu = menu self._tm = tm self._stop_event = stop_event self._ctx = ctx self.custom_ids: dict[str, base.BaseComponent[special_endpoints.MessageActionRowBuilder]] = { c.custom_id: c for row in self._menu._rows for c in row if not isinstance(c, LinkButton) } async def on_interaction( self, interaction: hikari.ComponentInteraction, initial_response_sent: asyncio.Event ) -> None: context = MenuContext( client=self._client, menu=self._menu, interaction=interaction, component=self.custom_ids[interaction.custom_id], _timeout=self._tm, _stop_event=self._stop_event, _initial_response_sent=initial_response_sent, ) token: contextvars.Token[linkd.Container | None] | None = None if self._ctx is not None: token = linkd.DI_CONTAINER.set(self._ctx.get(linkd.DI_CONTAINER)) try: if not await self._menu.predicate(context): return callback: t.Callable[[MenuContext], t.Awaitable[None]] = getattr(context.component, "callback") await callback(context) finally: if token is not None: linkd.DI_CONTAINER.reset(token) if self._stop_event.is_set(): self._client._attached_menus.discard(self) if self._stop_event.is_set(): return if context._should_re_resolve_custom_ids: self.custom_ids = {c.custom_id: c for row in self._menu._rows for c in row if not isinstance(c, LinkButton)}