Files
2026-02-03 20:32:43 +02:00

1625 lines
51 KiB
Python

# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
Final,
Generic,
List,
Literal,
Mapping,
Optional,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from .asset import AssetMixin
from .colour import Colour
from .enums import (
ButtonStyle,
ChannelType,
ComponentType,
SelectDefaultValueType,
SeparatorSpacing,
TextInputStyle,
try_enum,
)
from .partial_emoji import PartialEmoji, _EmojiTag
from .utils import MISSING, _get_as_snowflake, assert_never, get_slots
if TYPE_CHECKING:
from typing_extensions import Self, TypeAlias
from .emoji import Emoji
from .message import Attachment
from .types.components import (
ActionRow as ActionRowPayload,
AnySelectMenu as AnySelectMenuPayload,
BaseSelectMenu as BaseSelectMenuPayload,
ButtonComponent as ButtonComponentPayload,
ChannelSelectMenu as ChannelSelectMenuPayload,
Component as ComponentPayload,
ComponentType as ComponentTypeLiteral,
ContainerComponent as ContainerComponentPayload,
FileComponent as FileComponentPayload,
LabelComponent as LabelComponentPayload,
MediaGalleryComponent as MediaGalleryComponentPayload,
MediaGalleryItem as MediaGalleryItemPayload,
MentionableSelectMenu as MentionableSelectMenuPayload,
MessageTopLevelComponent as MessageTopLevelComponentPayload,
RoleSelectMenu as RoleSelectMenuPayload,
SectionComponent as SectionComponentPayload,
SelectDefaultValue as SelectDefaultValuePayload,
SelectOption as SelectOptionPayload,
SeparatorComponent as SeparatorComponentPayload,
StringSelectMenu as StringSelectMenuPayload,
TextDisplayComponent as TextDisplayComponentPayload,
TextInput as TextInputPayload,
ThumbnailComponent as ThumbnailComponentPayload,
UnfurledMediaItem as UnfurledMediaItemPayload,
UserSelectMenu as UserSelectMenuPayload,
)
__all__ = (
"Component",
"ActionRow",
"Button",
"BaseSelectMenu",
"StringSelectMenu",
"SelectMenu",
"UserSelectMenu",
"RoleSelectMenu",
"MentionableSelectMenu",
"ChannelSelectMenu",
"SelectOption",
"SelectDefaultValue",
"TextInput",
"Section",
"TextDisplay",
"UnfurledMediaItem",
"Thumbnail",
"MediaGallery",
"MediaGalleryItem",
"FileComponent",
"Separator",
"Container",
"Label",
)
# miscellaneous components-related type aliases
LocalMediaItemInput = Union[str, "UnfurledMediaItem"]
MediaItemInput = Union[LocalMediaItemInput, "AssetMixin", "Attachment"]
AnySelectMenu = Union[
"StringSelectMenu",
"UserSelectMenu",
"RoleSelectMenu",
"MentionableSelectMenu",
"ChannelSelectMenu",
]
SelectMenuType = Literal[
ComponentType.string_select,
ComponentType.user_select,
ComponentType.role_select,
ComponentType.mentionable_select,
ComponentType.channel_select,
]
# valid `ActionRow.components` item types in a message/modal
ActionRowMessageComponent = Union["Button", "AnySelectMenu"]
ActionRowModalComponent: TypeAlias = "TextInput"
# any child component type of action rows
ActionRowChildComponent = Union[ActionRowMessageComponent, ActionRowModalComponent]
ActionRowChildComponentT = TypeVar("ActionRowChildComponentT", bound=ActionRowChildComponent)
# valid `Section.accessory` types
SectionAccessoryComponent = Union["Thumbnail", "Button"]
# valid `Section.components` item types
SectionChildComponent: TypeAlias = "TextDisplay"
# valid `Container.components` item types
ContainerChildComponent = Union[
"ActionRow[ActionRowMessageComponent]",
"Section",
"TextDisplay",
"MediaGallery",
"FileComponent",
"Separator",
]
# valid `Label.component` types
LabelChildComponent = Union[
"TextInput",
"AnySelectMenu",
]
# valid `Message.components` item types (v1/v2)
MessageTopLevelComponentV1: TypeAlias = "ActionRow[ActionRowMessageComponent]"
MessageTopLevelComponentV2 = Union[
"Section",
"TextDisplay",
"MediaGallery",
"FileComponent",
"Separator",
"Container",
]
MessageTopLevelComponent = Union[MessageTopLevelComponentV1, MessageTopLevelComponentV2]
_SELECT_COMPONENT_TYPES = frozenset(
(
ComponentType.string_select,
ComponentType.user_select,
ComponentType.role_select,
ComponentType.mentionable_select,
ComponentType.channel_select,
)
)
# not using `_SELECT_COMPONENT_TYPES` since pyright wouldn't infer the literal properly
_SELECT_COMPONENT_TYPE_VALUES = frozenset(
(
ComponentType.string_select.value,
ComponentType.user_select.value,
ComponentType.role_select.value,
ComponentType.mentionable_select.value,
ComponentType.channel_select.value,
)
)
class Component:
"""Represents the base component that all other components inherit from.
The components supported by Discord are:
- :class:`ActionRow`
- :class:`Button`
- subtypes of :class:`BaseSelectMenu` (:class:`ChannelSelectMenu`, :class:`MentionableSelectMenu`, :class:`RoleSelectMenu`, :class:`StringSelectMenu`, :class:`UserSelectMenu`)
- :class:`TextInput`
- :class:`Section`
- :class:`TextDisplay`
- :class:`Thumbnail`
- :class:`MediaGallery`
- :class:`FileComponent`
- :class:`Separator`
- :class:`Container`
- :class:`Label`
This class is abstract and cannot be instantiated.
.. versionadded:: 2.0
Attributes
----------
type: :class:`ComponentType`
The type of component.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
If set to ``0`` (the default) when sending a component, the API will assign sequential
identifiers to the components in the message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ("type", "id")
__repr_attributes__: ClassVar[Tuple[str, ...]]
# subclasses are expected to overwrite this if they're only usable with `MessageFlags.is_components_v2`
is_v2: ClassVar[bool] = False
type: ComponentType
id: int
def __repr__(self) -> str:
attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_attributes__)
return f"<{self.__class__.__name__} {attrs}>"
@classmethod
def _raw_construct(cls, **kwargs) -> Self:
self = cls.__new__(cls)
for slot in get_slots(cls):
try:
value = kwargs[slot]
except KeyError:
pass
else:
setattr(self, slot, value)
return self
def to_dict(self) -> Dict[str, Any]:
raise NotImplementedError
class ActionRow(Component, Generic[ActionRowChildComponentT]):
"""Represents an action row.
This is a component that holds up to 5 children components in a row.
This inherits from :class:`Component`.
.. versionadded:: 2.0
Attributes
----------
children: List[Union[:class:`Button`, :class:`BaseSelectMenu`, :class:`TextInput`]]
The children components that this holds, if any.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ("children",)
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: ActionRowPayload) -> None:
self.type: Literal[ComponentType.action_row] = ComponentType.action_row
self.id = data.get("id", 0)
children = [_component_factory(d) for d in data.get("components", [])]
self.children: List[ActionRowChildComponentT] = children # type: ignore
def to_dict(self) -> ActionRowPayload:
return {
"type": self.type.value,
"id": self.id,
"components": [child.to_dict() for child in self.children],
}
class Button(Component):
"""Represents a button from the Discord Bot UI Kit.
This inherits from :class:`Component`.
.. note::
The user constructible and usable type to create a button is :class:`disnake.ui.Button`,
not this one.
.. versionadded:: 2.0
Attributes
----------
style: :class:`.ButtonStyle`
The style of the button.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
If this button is for a URL or an SKU, it does not have a custom ID.
url: Optional[:class:`str`]
The URL this button sends you to.
disabled: :class:`bool`
Whether the button is disabled or not.
label: Optional[:class:`str`]
The label of the button, if any.
emoji: Optional[:class:`PartialEmoji`]
The emoji of the button, if available.
sku_id: Optional[:class:`int`]
The ID of a purchasable SKU, for premium buttons.
Premium buttons additionally cannot have a ``label``, ``url``, or ``emoji``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = (
"style",
"custom_id",
"url",
"disabled",
"label",
"emoji",
"sku_id",
)
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: ButtonComponentPayload) -> None:
self.type: Literal[ComponentType.button] = ComponentType.button
self.id = data.get("id", 0)
self.style: ButtonStyle = try_enum(ButtonStyle, data["style"])
self.custom_id: Optional[str] = data.get("custom_id")
self.url: Optional[str] = data.get("url")
self.disabled: bool = data.get("disabled", False)
self.label: Optional[str] = data.get("label")
self.emoji: Optional[PartialEmoji]
try:
self.emoji = PartialEmoji.from_dict(data["emoji"])
except KeyError:
self.emoji = None
self.sku_id: Optional[int] = _get_as_snowflake(data, "sku_id")
def to_dict(self) -> ButtonComponentPayload:
payload: ButtonComponentPayload = {
"type": self.type.value,
"id": self.id,
"style": self.style.value,
"disabled": self.disabled,
}
if self.label:
payload["label"] = self.label
if self.custom_id:
payload["custom_id"] = self.custom_id
if self.url:
payload["url"] = self.url
if self.emoji:
payload["emoji"] = self.emoji.to_dict()
if self.sku_id:
payload["sku_id"] = self.sku_id
return payload
class BaseSelectMenu(Component):
"""Represents an abstract select menu from the Discord Bot UI Kit.
A select menu is functionally the same as a dropdown, however
on mobile it renders a bit differently.
The currently supported select menus are:
- :class:`~disnake.StringSelectMenu`
- :class:`~disnake.UserSelectMenu`
- :class:`~disnake.RoleSelectMenu`
- :class:`~disnake.MentionableSelectMenu`
- :class:`~disnake.ChannelSelectMenu`
.. versionadded:: 2.7
Attributes
----------
custom_id: Optional[:class:`str`]
The ID of the select menu that gets received during an interaction.
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:`SelectOption`]
A list of options that can be selected in this select menu.
disabled: :class:`bool`
Whether the select menu is disabled or not.
default_values: List[:class:`SelectDefaultValue`]
The list of values (users/roles/channels) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
Only available for auto-populated select menus.
.. versionadded:: 2.10
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = (
"custom_id",
"placeholder",
"min_values",
"max_values",
"disabled",
"default_values",
"required",
)
# FIXME: this isn't pretty; we should decouple __repr__ from slots
__repr_attributes__: ClassVar[Tuple[str, ...]] = tuple(
s for s in __slots__ if s != "default_values"
)
# n.b: ideally this would be `BaseSelectMenuPayload`,
# but pyright made TypedDict keys invariant and doesn't
# fully support readonly items yet (which would help avoid this)
def __init__(self, data: AnySelectMenuPayload) -> None:
component_type = try_enum(ComponentType, data["type"])
self.type: SelectMenuType = component_type # type: ignore
self.id = data.get("id", 0)
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.disabled: bool = data.get("disabled", False)
self.default_values: List[SelectDefaultValue] = [
SelectDefaultValue._from_dict(d) for d in (data.get("default_values") or [])
]
self.required: bool = data.get("required", True)
def to_dict(self) -> BaseSelectMenuPayload:
payload: BaseSelectMenuPayload = {
"type": self.type.value,
"id": self.id,
"custom_id": self.custom_id,
"min_values": self.min_values,
"max_values": self.max_values,
"disabled": self.disabled,
"required": self.required,
}
if self.placeholder:
payload["placeholder"] = self.placeholder
if self.default_values:
payload["default_values"] = [v.to_dict() for v in self.default_values]
return payload
class StringSelectMenu(BaseSelectMenu):
"""Represents a string select menu from the Discord Bot UI Kit.
.. note::
The user constructible and usable type to create a
string select menu is :class:`disnake.ui.StringSelect`.
.. versionadded:: 2.0
.. versionchanged:: 2.7
Renamed from ``SelectMenu`` to ``StringSelectMenu``.
Attributes
----------
custom_id: Optional[:class:`str`]
The ID of the select menu that gets received during an interaction.
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.
disabled: :class:`bool`
Whether the select menu is disabled or not.
options: List[:class:`SelectOption`]
A list of options that can be selected in this select menu.
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ("options",)
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
*BaseSelectMenu.__repr_attributes__,
*__slots__,
)
type: Literal[ComponentType.string_select]
def __init__(self, data: StringSelectMenuPayload) -> None:
super().__init__(data)
self.options: List[SelectOption] = [
SelectOption.from_dict(option) for option in data.get("options", [])
]
def to_dict(self) -> StringSelectMenuPayload:
payload = cast("StringSelectMenuPayload", super().to_dict())
payload["options"] = [op.to_dict() for op in self.options]
return payload
SelectMenu = StringSelectMenu # backwards compatibility
class UserSelectMenu(BaseSelectMenu):
"""Represents a user select menu from the Discord Bot UI Kit.
.. note::
The user constructible and usable type to create a
user select menu is :class:`disnake.ui.UserSelect`.
.. versionadded:: 2.7
Attributes
----------
custom_id: Optional[:class:`str`]
The ID of the select menu that gets received during an interaction.
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.
disabled: :class:`bool`
Whether the select menu is disabled or not.
default_values: List[:class:`SelectDefaultValue`]
The list of values (users/members) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
.. versionadded:: 2.10
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ()
type: Literal[ComponentType.user_select]
if TYPE_CHECKING:
def to_dict(self) -> UserSelectMenuPayload:
return cast("UserSelectMenuPayload", super().to_dict())
class RoleSelectMenu(BaseSelectMenu):
"""Represents a role select menu from the Discord Bot UI Kit.
.. note::
The user constructible and usable type to create a
role select menu is :class:`disnake.ui.RoleSelect`.
.. versionadded:: 2.7
Attributes
----------
custom_id: Optional[:class:`str`]
The ID of the select menu that gets received during an interaction.
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.
disabled: :class:`bool`
Whether the select menu is disabled or not.
default_values: List[:class:`SelectDefaultValue`]
The list of values (roles) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
.. versionadded:: 2.10
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ()
type: Literal[ComponentType.role_select]
if TYPE_CHECKING:
def to_dict(self) -> RoleSelectMenuPayload:
return cast("RoleSelectMenuPayload", super().to_dict())
class MentionableSelectMenu(BaseSelectMenu):
"""Represents a mentionable (user/member/role) select menu from the Discord Bot UI Kit.
.. note::
The user constructible and usable type to create a
mentionable select menu is :class:`disnake.ui.MentionableSelect`.
.. versionadded:: 2.7
Attributes
----------
custom_id: Optional[:class:`str`]
The ID of the select menu that gets received during an interaction.
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.
disabled: :class:`bool`
Whether the select menu is disabled or not.
default_values: List[:class:`SelectDefaultValue`]
The list of values (users/roles) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
.. versionadded:: 2.10
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ()
type: Literal[ComponentType.mentionable_select]
if TYPE_CHECKING:
def to_dict(self) -> MentionableSelectMenuPayload:
return cast("MentionableSelectMenuPayload", super().to_dict())
class ChannelSelectMenu(BaseSelectMenu):
"""Represents a channel select menu from the Discord Bot UI Kit.
.. note::
The user constructible and usable type to create a
channel select menu is :class:`disnake.ui.ChannelSelect`.
.. versionadded:: 2.7
Attributes
----------
custom_id: Optional[:class:`str`]
The ID of the select menu that gets received during an interaction.
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.
disabled: :class:`bool`
Whether the select menu is disabled or not.
channel_types: Optional[List[:class:`ChannelType`]]
A list of channel types that can be selected in this select menu.
If ``None``, channels of all types may be selected.
default_values: List[:class:`SelectDefaultValue`]
The list of values (channels) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
.. versionadded:: 2.10
required: :class:`bool`
Whether the select menu is required. Only applies to components in modals.
Defaults to ``True``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ("channel_types",)
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
*BaseSelectMenu.__repr_attributes__,
*__slots__,
)
type: Literal[ComponentType.channel_select]
def __init__(self, data: ChannelSelectMenuPayload) -> None:
super().__init__(data)
# on the API side, an empty list is (currently) equivalent to no value
channel_types = data.get("channel_types")
self.channel_types: Optional[List[ChannelType]] = (
[try_enum(ChannelType, t) for t in channel_types] if channel_types else None
)
def to_dict(self) -> ChannelSelectMenuPayload:
payload = cast("ChannelSelectMenuPayload", super().to_dict())
if self.channel_types:
payload["channel_types"] = [t.value for t in self.channel_types]
return payload
class SelectOption:
"""Represents a string select menu's option.
These can be created by users.
.. versionadded:: 2.0
Attributes
----------
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 provided when constructed then it 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.
default: :class:`bool`
Whether this option is selected by default.
"""
__slots__: Tuple[str, ...] = (
"label",
"value",
"description",
"emoji",
"default",
)
def __init__(
self,
*,
label: str,
value: str = MISSING,
description: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
default: bool = False,
) -> None:
self.label = label
self.value = label if value is MISSING else value
self.description = description
if emoji is not None:
if isinstance(emoji, str):
emoji = PartialEmoji.from_str(emoji)
elif isinstance(emoji, _EmojiTag):
emoji = emoji._to_partial()
else:
raise TypeError(
f"expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}"
)
self.emoji = emoji
self.default = default
def __repr__(self) -> str:
return (
f"<SelectOption label={self.label!r} value={self.value!r} description={self.description!r} "
f"emoji={self.emoji!r} default={self.default!r}>"
)
def __str__(self) -> str:
if self.emoji:
base = f"{self.emoji} {self.label}"
else:
base = self.label
if self.description:
return f"{base}\n{self.description}"
return base
@classmethod
def from_dict(cls, data: SelectOptionPayload) -> SelectOption:
try:
emoji = PartialEmoji.from_dict(data["emoji"])
except KeyError:
emoji = None
return cls(
label=data["label"],
value=data["value"],
description=data.get("description"),
emoji=emoji,
default=data.get("default", False),
)
def to_dict(self) -> SelectOptionPayload:
payload: SelectOptionPayload = {
"label": self.label,
"value": self.value,
"default": self.default,
}
if self.emoji:
payload["emoji"] = self.emoji.to_dict()
if self.description:
payload["description"] = self.description
return payload
class SelectDefaultValue:
"""Represents a default value of an auto-populated select menu (currently all
select menu types except :class:`StringSelectMenu`).
Depending on the :attr:`type` attribute, this can represent different types of objects.
.. versionadded:: 2.10
Attributes
----------
id: :class:`int`
The ID of the target object.
type: :class:`SelectDefaultValueType`
The type of the target object.
"""
__slots__: Tuple[str, ...] = ("id", "type")
def __init__(self, id: int, type: SelectDefaultValueType) -> None:
self.id: int = id
self.type: SelectDefaultValueType = type
@classmethod
def _from_dict(cls, data: SelectDefaultValuePayload) -> Self:
return cls(int(data["id"]), try_enum(SelectDefaultValueType, data["type"]))
def to_dict(self) -> SelectDefaultValuePayload:
return {
"id": self.id,
"type": self.type.value,
}
def __repr__(self) -> str:
return f"<SelectDefaultValue id={self.id!r} type={self.type.value!r}>"
class TextInput(Component):
"""Represents a text input from the Discord Bot UI Kit.
.. versionadded:: 2.4
.. note::
The user constructible and usable type to create a text input is
:class:`disnake.ui.TextInput`, not this one.
Attributes
----------
style: :class:`TextInputStyle`
The style of the text input.
label: Optional[:class:`str`]
The label of the text input.
.. deprecated:: 2.11
Deprecated in favor of :class:`Label`.
custom_id: :class:`str`
The ID of the text input that gets received during an interaction.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is entered.
value: Optional[:class:`str`]
The pre-filled text of the text input.
required: :class:`bool`
Whether the text input is required. Defaults to ``True``.
min_length: Optional[:class:`int`]
The minimum length of the text input.
max_length: Optional[:class:`int`]
The maximum length of the text input.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = (
"style",
"custom_id",
"label",
"placeholder",
"value",
"required",
"max_length",
"min_length",
)
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: TextInputPayload) -> None:
self.type: Literal[ComponentType.text_input] = ComponentType.text_input
self.id = data.get("id", 0)
self.custom_id: str = data["custom_id"]
self.style: TextInputStyle = try_enum(
TextInputStyle, data.get("style", TextInputStyle.short.value)
)
self.label: Optional[str] = data.get("label") # deprecated
self.placeholder: Optional[str] = data.get("placeholder")
self.value: Optional[str] = data.get("value")
self.required: bool = data.get("required", True)
self.min_length: Optional[int] = data.get("min_length")
self.max_length: Optional[int] = data.get("max_length")
def to_dict(self) -> TextInputPayload:
payload: TextInputPayload = {
"type": self.type.value,
"id": self.id,
"style": self.style.value,
"label": self.label,
"custom_id": self.custom_id,
"required": self.required,
}
if self.placeholder is not None:
payload["placeholder"] = self.placeholder
if self.value is not None:
payload["value"] = self.value
if self.min_length is not None:
payload["min_length"] = self.min_length
if self.max_length is not None:
payload["max_length"] = self.max_length
return payload
class Section(Component):
"""Represents a section from the Discord Bot UI Kit (v2).
This allows displaying an accessory (thumbnail or button) next to a block of text.
.. note::
The user constructible and usable type to create a
section is :class:`disnake.ui.Section`.
.. versionadded:: 2.11
Attributes
----------
children: List[:class:`TextDisplay`]
The text items in this section.
accessory: Union[:class:`Thumbnail`, :class:`Button`]
The accessory component displayed next to the section text.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ("children", "accessory")
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
is_v2 = True
def __init__(self, data: SectionComponentPayload) -> None:
self.type: Literal[ComponentType.section] = ComponentType.section
self.id = data.get("id", 0)
self.children: List[SectionChildComponent] = [
_component_factory(d, type=SectionChildComponent) for d in data.get("components", [])
]
accessory = _component_factory(data["accessory"])
self.accessory: SectionAccessoryComponent = accessory # type: ignore
def to_dict(self) -> SectionComponentPayload:
return {
"type": self.type.value,
"id": self.id,
"accessory": self.accessory.to_dict(),
"components": [child.to_dict() for child in self.children],
}
class TextDisplay(Component):
"""Represents a text display from the Discord Bot UI Kit (v2).
.. note::
The user constructible and usable type to create a
text display is :class:`disnake.ui.TextDisplay`.
.. versionadded:: 2.11
Attributes
----------
content: :class:`str`
The text displayed by this component.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ("content",)
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
is_v2 = True
def __init__(self, data: TextDisplayComponentPayload) -> None:
self.type: Literal[ComponentType.text_display] = ComponentType.text_display
self.id = data.get("id", 0)
self.content: str = data["content"]
def to_dict(self) -> TextDisplayComponentPayload:
return {
"type": self.type.value,
"id": self.id,
"content": self.content,
}
class UnfurledMediaItem:
"""Represents an unfurled/resolved media item within a component.
.. versionadded:: 2.11
Attributes
----------
url: :class:`str`
The URL of this media item.
proxy_url: :class:`str`
The proxied URL of this media item. This is a cached version of
the :attr:`url` in the case of images.
height: Optional[:class:`int`]
The height of this media item, if applicable.
width: Optional[:class:`int`]
The width of this media item, if applicable.
content_type: Optional[:class:`str`]
The `media type <https://en.wikipedia.org/wiki/Media_type>`_ of this media item.
attachment_id: Optional[:class:`int`]
The ID of the uploaded attachment. Only present if the media item was
uploaded as an attachment.
"""
__slots__: Tuple[str, ...] = (
"url",
"proxy_url",
"height",
"width",
"content_type",
"attachment_id",
)
# generally, users should also be able to pass a plain url where applicable instead of
# an UnfurledMediaItem instance; this is largely for internal use
def __init__(self, url: str) -> None:
self.url: str = url
self.proxy_url: Optional[str] = None
self.height: Optional[int] = None
self.width: Optional[int] = None
self.content_type: Optional[str] = None
self.attachment_id: Optional[int] = None
@classmethod
def from_dict(cls, data: UnfurledMediaItemPayload) -> Self:
self = cls(data["url"])
self.proxy_url = data.get("proxy_url")
self.height = _get_as_snowflake(data, "height")
self.width = _get_as_snowflake(data, "width")
self.content_type = data.get("content_type")
self.attachment_id = _get_as_snowflake(data, "attachment_id")
return self
def to_dict(self) -> UnfurledMediaItemPayload:
# for sending, only `url` is required, and other fields are ignored regardless
return {"url": self.url}
def __repr__(self) -> str:
return f"<UnfurledMediaItem url={self.url} content_type={self.content_type}>"
class Thumbnail(Component):
"""Represents a thumbnail from the Discord Bot UI Kit (v2).
This is only supported as the :attr:`~Section.accessory` of a section component.
.. note::
The user constructible and usable type to create a
thumbnail is :class:`disnake.ui.Thumbnail`.
.. versionadded:: 2.11
Attributes
----------
media: :class:`UnfurledMediaItem`
The media item to display. Can be an arbitrary URL or attachment
reference (``attachment://<filename>``).
description: Optional[:class:`str`]
The thumbnail's description ("alt text"), if any.
spoiler: :class:`bool`
Whether the thumbnail is marked as a spoiler. Defaults to ``False``.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = (
"media",
"description",
"spoiler",
)
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
is_v2 = True
def __init__(self, data: ThumbnailComponentPayload) -> None:
self.type: Literal[ComponentType.thumbnail] = ComponentType.thumbnail
self.id = data.get("id", 0)
self.media: UnfurledMediaItem = UnfurledMediaItem.from_dict(data["media"])
self.description: Optional[str] = data.get("description")
self.spoiler: bool = data.get("spoiler", False)
def to_dict(self) -> ThumbnailComponentPayload:
payload: ThumbnailComponentPayload = {
"type": self.type.value,
"id": self.id,
"media": self.media.to_dict(),
"spoiler": self.spoiler,
}
if self.description:
payload["description"] = self.description
return payload
class MediaGallery(Component):
"""Represents a media gallery from the Discord Bot UI Kit (v2).
This allows displaying up to 10 images in a gallery.
.. note::
The user constructible and usable type to create a
media gallery is :class:`disnake.ui.MediaGallery`.
.. versionadded:: 2.11
Attributes
----------
items: List[:class:`MediaGalleryItem`]
The images in this gallery.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ("items",)
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
is_v2 = True
def __init__(self, data: MediaGalleryComponentPayload) -> None:
self.type: Literal[ComponentType.media_gallery] = ComponentType.media_gallery
self.id = data.get("id", 0)
self.items: List[MediaGalleryItem] = [MediaGalleryItem.from_dict(i) for i in data["items"]]
def to_dict(self) -> MediaGalleryComponentPayload:
return {
"type": self.type.value,
"id": self.id,
"items": [i.to_dict() for i in self.items],
}
class MediaGalleryItem:
"""Represents an item inside a :class:`MediaGallery`.
.. versionadded:: 2.11
Parameters
----------
media: Union[:class:`str`, :class:`.Asset`, :class:`.Attachment`, :class:`.UnfurledMediaItem`]
The media item to display. Can be an arbitrary URL or attachment
reference (``attachment://<filename>``).
description: Optional[:class:`str`]
The item's description ("alt text"), if any.
spoiler: :class:`bool`
Whether the item is marked as a spoiler. Defaults to ``False``.
"""
__slots__: Tuple[str, ...] = (
"media",
"description",
"spoiler",
)
def __init__(
self,
media: MediaItemInput,
description: Optional[str] = None,
*,
spoiler: bool = False,
) -> None:
self.media: UnfurledMediaItem = handle_media_item_input(media)
self.description: Optional[str] = description
self.spoiler: bool = spoiler
@classmethod
def from_dict(cls, data: MediaGalleryItemPayload) -> Self:
return cls(
media=UnfurledMediaItem.from_dict(data["media"]),
description=data.get("description"),
spoiler=data.get("spoiler", False),
)
def to_dict(self) -> MediaGalleryItemPayload:
payload: MediaGalleryItemPayload = {
"media": self.media.to_dict(),
"spoiler": self.spoiler,
}
if self.description:
payload["description"] = self.description
return payload
def __repr__(self) -> str:
return f"<MediaGalleryItem media={self.media!r} description={self.description!r}>"
class FileComponent(Component):
"""Represents a file component from the Discord Bot UI Kit (v2).
This allows displaying attached files.
.. note::
The user constructible and usable type to create a
file component is :class:`disnake.ui.File`.
.. versionadded:: 2.11
Attributes
----------
file: :class:`UnfurledMediaItem`
The file to display. This **only** supports attachment references (i.e.
using the ``attachment://<filename>`` syntax), not arbitrary URLs.
spoiler: :class:`bool`
Whether the file is marked as a spoiler. Defaults to ``False``.
name: Optional[:class:`str`]
The name of the file.
This is available in objects from the API, and ignored when sending.
size: Optional[:class:`int`]
The size of the file.
This is available in objects from the API, and ignored when sending.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ("file", "spoiler", "name", "size")
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
is_v2 = True
def __init__(self, data: FileComponentPayload) -> None:
self.type: Literal[ComponentType.file] = ComponentType.file
self.id = data.get("id", 0)
self.file: UnfurledMediaItem = UnfurledMediaItem.from_dict(data["file"])
self.spoiler: bool = data.get("spoiler", False)
self.name: Optional[str] = data.get("name")
self.size: Optional[int] = data.get("size")
def to_dict(self) -> FileComponentPayload:
return {
"type": self.type.value,
"id": self.id,
"file": self.file.to_dict(),
"spoiler": self.spoiler,
}
class Separator(Component):
"""Represents a separator from the Discord Bot UI Kit (v2).
This allows vertically separating components.
.. note::
The user constructible and usable type to create a
separator is :class:`disnake.ui.Separator`.
.. versionadded:: 2.11
Attributes
----------
divider: :class:`bool`
Whether the separator should be visible, instead of just being vertical padding/spacing.
Defaults to ``True``.
spacing: :class:`SeparatorSpacing`
The size of the separator padding.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = ("divider", "spacing")
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
is_v2 = True
def __init__(self, data: SeparatorComponentPayload) -> None:
self.type: Literal[ComponentType.separator] = ComponentType.separator
self.id = data.get("id", 0)
self.divider: bool = data.get("divider", True)
self.spacing: SeparatorSpacing = try_enum(SeparatorSpacing, data.get("spacing", 1))
def to_dict(self) -> SeparatorComponentPayload:
return {
"type": self.type.value,
"id": self.id,
"divider": self.divider,
"spacing": self.spacing.value,
}
class Container(Component):
"""Represents a container from the Discord Bot UI Kit (v2).
This is visually similar to :class:`Embed`\\s, and contains other components.
.. note::
The user constructible and usable type to create a
container is :class:`disnake.ui.Container`.
.. versionadded:: 2.11
Attributes
----------
children: List[Union[:class:`ActionRow`, :class:`Section`, :class:`TextDisplay`, :class:`MediaGallery`, :class:`FileComponent`, :class:`Separator`]]
The child components in this container.
accent_colour: Optional[:class:`Colour`]
The accent colour of the container. An alias exists under ``accent_color``.
spoiler: :class:`bool`
Whether the container is marked as a spoiler. Defaults to ``False``.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
.. versionadded:: 2.11
"""
__slots__: Tuple[str, ...] = (
"children",
"accent_colour",
"spoiler",
)
__repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__
is_v2 = True
def __init__(self, data: ContainerComponentPayload) -> None:
self.type: Literal[ComponentType.container] = ComponentType.container
self.id = data.get("id", 0)
components = [_component_factory(d) for d in data.get("components", [])]
self.children: List[ContainerChildComponent] = components # type: ignore
self.accent_colour: Optional[Colour] = (
Colour(accent_color) if (accent_color := data.get("accent_color")) is not None else None
)
self.spoiler: bool = data.get("spoiler", False)
def to_dict(self) -> ContainerComponentPayload:
payload: ContainerComponentPayload = {
"type": self.type.value,
"id": self.id,
"spoiler": self.spoiler,
"components": [child.to_dict() for child in self.children],
}
if self.accent_colour is not None:
payload["accent_color"] = self.accent_colour.value
return payload
@property
def accent_color(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: The accent color of the container.
An alias exists under ``accent_colour``.
"""
return self.accent_colour
class Label(Component):
"""Represents a label from the Discord Bot UI Kit.
This wraps other components with a label and an optional description,
and can only be used in modals.
.. versionadded:: 2.11
.. note::
The user constructible and usable type to create a label is
:class:`disnake.ui.Label`, not this one.
Attributes
----------
text: :class:`str`
The label text.
description: Optional[:class:`str`]
The description text for the label.
component: Union[:class:`TextInput`, :class:`StringSelectMenu`]
The component within the label.
id: :class:`int`
The numeric identifier for the component.
This is always present in components received from the API,
and unique within a message.
"""
__slots__: Tuple[str, ...] = (
"text",
"description",
"component",
)
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: LabelComponentPayload) -> None:
self.type: Literal[ComponentType.label] = ComponentType.label
self.id = data.get("id", 0)
self.text: str = data["label"]
self.description: Optional[str] = data.get("description")
component = _component_factory(data["component"])
self.component: LabelChildComponent = component # type: ignore
def to_dict(self) -> LabelComponentPayload:
payload: LabelComponentPayload = {
"type": self.type.value,
"id": self.id,
"label": self.text,
"component": self.component.to_dict(),
}
if self.description is not None:
payload["description"] = self.description
return payload
# types of components that are allowed in a message's action rows;
# see also `ActionRowMessageComponent` type alias
VALID_ACTION_ROW_MESSAGE_COMPONENT_TYPES: Final = (
Button,
StringSelectMenu,
UserSelectMenu,
RoleSelectMenu,
MentionableSelectMenu,
ChannelSelectMenu,
)
def handle_media_item_input(value: MediaItemInput) -> UnfurledMediaItem:
if isinstance(value, UnfurledMediaItem):
return value
elif isinstance(value, str):
return UnfurledMediaItem(value)
# circular import
from .message import Attachment
if isinstance(value, (AssetMixin, Attachment)):
return UnfurledMediaItem(value.url)
assert_never(value)
raise TypeError(f"{type(value).__name__} cannot be converted to UnfurledMediaItem")
C = TypeVar("C", bound="Component")
COMPONENT_LOOKUP: Mapping[ComponentTypeLiteral, Type[Component]] = {
ComponentType.action_row.value: ActionRow,
ComponentType.button.value: Button,
ComponentType.string_select.value: StringSelectMenu,
ComponentType.text_input.value: TextInput,
ComponentType.user_select.value: UserSelectMenu,
ComponentType.role_select.value: RoleSelectMenu,
ComponentType.mentionable_select.value: MentionableSelectMenu,
ComponentType.channel_select.value: ChannelSelectMenu,
ComponentType.section.value: Section,
ComponentType.text_display.value: TextDisplay,
ComponentType.thumbnail.value: Thumbnail,
ComponentType.media_gallery.value: MediaGallery,
ComponentType.file.value: FileComponent,
ComponentType.separator.value: Separator,
ComponentType.container.value: Container,
ComponentType.label.value: Label,
}
# NOTE: The type param is purely for type-checking, it has no implications on runtime behavior.
# FIXME: could be improved with https://peps.python.org/pep-0747/
def _component_factory(data: ComponentPayload, *, type: Type[C] = Component) -> C:
component_type = data["type"]
try:
component_cls = COMPONENT_LOOKUP[component_type]
except KeyError:
# if we encounter an unknown component type, just construct a placeholder component for it
as_enum = try_enum(ComponentType, component_type)
return Component._raw_construct(type=as_enum) # type: ignore
else:
return component_cls(data) # type: ignore
# this is just a rebranded _component_factory, as a workaround to Python not supporting typescript-like mapped types
if TYPE_CHECKING:
def _message_component_factory(
data: MessageTopLevelComponentPayload,
) -> MessageTopLevelComponent: ...
else:
_message_component_factory = _component_factory