Initialisation du repository de Beta

This commit is contained in:
Mathis 2026-02-06 22:23:20 +01:00
commit 14985f6dbb
9469 changed files with 1903273 additions and 0 deletions

View file

@ -0,0 +1,27 @@
"""
discord.ui
~~~~~~~~~~~
Bot UI Kit helper for the Discord API
:copyright: (c) 2015-present Rapptz
:license: MIT, see LICENSE for more details.
"""
from .view import *
from .modal import *
from .item import *
from .button import *
from .select import *
from .text_input import *
from .dynamic import *
from .container import *
from .file import *
from .media_gallery import *
from .section import *
from .separator import *
from .text_display import *
from .thumbnail import *
from .action_row import *
from .label import *

View file

@ -0,0 +1,599 @@
"""
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 (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Coroutine,
Dict,
Generator,
List,
Literal,
Optional,
Sequence,
Type,
TypeVar,
Union,
overload,
)
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from .button import Button, button as _button
from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect
from ..components import ActionRow as ActionRowComponent
from ..enums import ButtonStyle, ComponentType, ChannelType
from ..partial_emoji import PartialEmoji
from ..utils import MISSING, get as _utils_get
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
from .select import (
BaseSelectT,
ValidDefaultValues,
MentionableSelectT,
ChannelSelectT,
RoleSelectT,
UserSelectT,
SelectT,
)
from ..emoji import Emoji
from ..components import SelectOption
from ..interactions import Interaction
from .container import Container
from .dynamic import DynamicItem
SelectCallbackDecorator = Callable[[ItemCallbackType['S', BaseSelectT]], BaseSelectT]
S = TypeVar('S', bound=Union['ActionRow', 'Container', 'LayoutView'], covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('ActionRow',)
class _ActionRowCallback:
__slots__ = ('row', 'callback', 'item')
def __init__(self, callback: ItemCallbackType[S, Any], row: ActionRow, item: Item[Any]) -> None:
self.callback: ItemCallbackType[Any, Any] = callback
self.row: ActionRow = row
self.item: Item[Any] = item
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
return self.callback(self.row, interaction, self.item)
class ActionRow(Item[V]):
r"""Represents a UI action row.
This is a top-level layout component that can only be used on :class:`LayoutView`
and can contain :class:`Button`\s and :class:`Select`\s in it.
Action rows can only have 5 children. This can be inherited.
.. versionadded:: 2.6
Examples
--------
.. code-block:: python3
import discord
from discord import ui
# you can subclass it and add components with the decorators
class MyActionRow(ui.ActionRow):
@ui.button(label='Click Me!')
async def click_me(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message('You clicked me!')
# or use it directly on LayoutView
class MyView(ui.LayoutView):
row = ui.ActionRow()
# or you can use your subclass:
# row = MyActionRow()
# you can add items with row.button and row.select
@row.button(label='A button!')
async def row_button(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message('You clicked a button!')
Parameters
----------
\*children: :class:`Item`
The initial children of this action row.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__action_row_children_items__: ClassVar[List[ItemCallbackType[Self, Any]]] = []
__discord_ui_action_row__: ClassVar[bool] = True
__item_repr_attributes__ = ('id',)
def __init__(
self,
*children: Item[V],
id: Optional[int] = None,
) -> None:
super().__init__()
self._children: List[Item[V]] = self._init_children()
self._children.extend(children)
self._weight: int = sum(i.width for i in self._children)
if self._weight > 5:
raise ValueError('maximum number of children exceeded')
self.id = id
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, ItemCallbackType[Self, Any]] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if hasattr(member, '__discord_ui_model_type__'):
children[name] = member
if len(children) > 5:
raise TypeError('ActionRow cannot have more than 5 children')
cls.__action_row_children_items__ = list(children.values())
def __repr__(self) -> str:
return f'<{self.__class__.__name__} children={len(self._children)}>'
def _init_children(self) -> List[Item[Any]]:
children = []
for func in self.__action_row_children_items__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ActionRowCallback(func, self, item) # type: ignore
item._parent = getattr(func, '__discord_ui_parent__', self)
setattr(self, func.__name__, item)
children.append(item)
return children
def _update_view(self, view) -> None:
self._view = view
for child in self._children:
child._view = view
def _has_children(self):
return True
def _is_v2(self) -> bool:
# although it is not really a v2 component the only usecase here is for
# LayoutView which basically represents the top-level payload of components
# and ActionRow is only allowed there anyways.
# If the user tries to add any V2 component to a View instead of LayoutView
# it should error anyways.
return True
def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None:
child_index = self._children.index(base)
self._children[child_index] = new # type: ignore
@property
def width(self):
return 5
@property
def _total_count(self) -> int:
# 1 for self and all children
return 1 + len(self._children)
@property
def type(self) -> Literal[ComponentType.action_row]:
return ComponentType.action_row
@property
def children(self) -> List[Item[V]]:
"""List[:class:`Item`]: The list of children attached to this action row."""
return self._children.copy()
def walk_children(self) -> Generator[Item[V], Any, None]:
"""An iterator that recursively walks through all the children of this action row
and its children, if applicable.
Yields
------
:class:`Item`
An item in the action row.
"""
for child in self.children:
yield child
def content_length(self) -> int:
""":class:`int`: Returns the total length of all text content in this action row."""
from .text_display import TextDisplay
return sum(len(item.content) for item in self._children if isinstance(item, TextDisplay))
def add_item(self, item: Item[Any]) -> Self:
"""Adds an item to this action row.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to add to the action row.
Raises
------
TypeError
An :class:`Item` was not passed.
ValueError
Maximum number of children has been exceeded (5)
or (40) for the entire view.
"""
if (self._weight + item.width) > 5:
raise ValueError('maximum number of children exceeded')
if len(self._children) >= 5:
raise ValueError('maximum number of children exceeded')
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__.__name__}')
if self._view:
self._view._add_count(1)
item._update_view(self.view)
item._parent = self
self._weight += 1
self._children.append(item)
return self
def remove_item(self, item: Item[Any]) -> Self:
"""Removes an item from the action row.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to remove from the action row.
"""
try:
self._children.remove(item)
except ValueError:
pass
else:
if self._view:
self._view._add_count(-1)
self._weight -= 1
return self
def find_item(self, id: int, /) -> Optional[Item[V]]:
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
not found.
.. warning::
This is **not the same** as ``custom_id``.
Parameters
----------
id: :class:`int`
The ID of the component.
Returns
-------
Optional[:class:`Item`]
The item found, or ``None``.
"""
return _utils_get(self.walk_children(), id=id)
def clear_items(self) -> Self:
"""Removes all items from the action row.
This function returns the class instance to allow for fluent-style
chaining.
"""
if self._view:
self._view._add_count(-len(self._children))
self._children.clear()
self._weight = 0
return self
def to_component_dict(self) -> Dict[str, Any]:
components = []
for component in self.children:
components.append(component.to_component_dict())
base = {
'type': self.type.value,
'components': components,
}
if self.id is not None:
base['id'] = self.id
return base
def button(
self,
*,
label: Optional[str] = None,
custom_id: Optional[str] = None,
disabled: bool = False,
style: ButtonStyle = ButtonStyle.secondary,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
id: Optional[int] = None,
) -> Callable[[ItemCallbackType[S, Button[V]]], Button[V]]:
"""A decorator that attaches a button to the action row.
The function being decorated should have three parameters, ``self`` representing
the :class:`discord.ui.ActionRow`, the :class:`discord.Interaction` you receive and
the :class:`discord.ui.Button` being pressed.
.. note::
Buttons with a URL or a SKU cannot be created with this function.
Consider creating a :class:`Button` manually and adding it via
:meth:`ActionRow.add_item` instead. This is beacuse these buttons
cannot have a callback associated with them since Discord does not
do any processing with them.
Parameters
----------
label: Optional[:class:`str`]
The label of the button, if any.
Can only be up to 80 characters.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
It is recommended to not set this parameters to prevent conflicts.
Can only be up to 100 characters.
style: :class:`.ButtonStyle`
The style of the button. Defaults to :attr:`.ButtonStyle.grey`.
disabled: :class:`bool`
Whether the button is disabled or not. Defaults to ``False``.
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
The emoji of the button. This can be in string form or a :class:`.PartialEmoji`
or a full :class:`.Emoji`.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
def decorator(func: ItemCallbackType[S, Button[V]]) -> ItemCallbackType[S, Button[V]]:
ret = _button(
label=label,
custom_id=custom_id,
disabled=disabled,
style=style,
emoji=emoji,
row=None,
id=id,
)(func)
ret.__discord_ui_parent__ = self # type: ignore
return ret # type: ignore
return decorator # type: ignore
@overload
def select(
self,
*,
cls: Type[SelectT] = Select[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = ...,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, SelectT]: ...
@overload
def select(
self,
*,
cls: Type[UserSelectT] = UserSelect[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = ...,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, UserSelectT]: ...
@overload
def select(
self,
*,
cls: Type[RoleSelectT] = RoleSelect[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = ...,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, RoleSelectT]: ...
@overload
def select(
self,
*,
cls: Type[ChannelSelectT] = ChannelSelect[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = ...,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, ChannelSelectT]: ...
@overload
def select(
self,
*,
cls: Type[MentionableSelectT] = MentionableSelect[Any],
options: List[SelectOption] = MISSING,
channel_types: List[ChannelType] = MISSING,
placeholder: Optional[str] = ...,
custom_id: str = ...,
min_values: int = ...,
max_values: int = ...,
disabled: bool = ...,
default_values: Sequence[ValidDefaultValues] = ...,
id: Optional[int] = ...,
) -> SelectCallbackDecorator[S, MentionableSelectT]: ...
def select(
self,
*,
cls: Type[BaseSelectT] = Select[Any],
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,
disabled: bool = False,
default_values: Sequence[ValidDefaultValues] = MISSING,
id: Optional[int] = None,
) -> SelectCallbackDecorator[S, BaseSelectT]:
"""A decorator that attaches a select menu to the action row.
The function being decorated should have three parameters, ``self`` representing
the :class:`discord.ui.ActionRow`, the :class:`discord.Interaction` you receive and
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`]] |
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
Example
---------
.. code-block:: python3
class MyView(discord.ui.LayoutView):
action_row = discord.ui.ActionRow()
@action_row.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.
Can only be up to 150 characters.
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.
Can only be up to 100 characters.
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. This can only be used with
:class:`Select` instances.
Can only contain up to 25 items.
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``.
default_values: Sequence[:class:`~discord.abc.Snowflake`]
A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances.
If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor.
Number of items must be in range of ``min_values`` and ``max_values``.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
def decorator(func: ItemCallbackType[S, BaseSelectT]) -> ItemCallbackType[S, BaseSelectT]:
r = _select( # type: ignore
cls=cls, # type: ignore
placeholder=placeholder,
custom_id=custom_id,
min_values=min_values,
max_values=max_values,
options=options,
channel_types=channel_types,
disabled=disabled,
default_values=default_values,
id=id,
)(func)
r.__discord_ui_parent__ = self
return r
return decorator # type: ignore
@classmethod
def from_component(cls, component: ActionRowComponent) -> ActionRow:
from .view import _component_to_item
self = cls(id=component.id)
for cmp in component.children:
self.add_item(_component_to_item(cmp, self))
return self

View file

@ -0,0 +1,387 @@
"""
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
import copy
from typing import Callable, Literal, Optional, TYPE_CHECKING, Tuple, TypeVar, Union
import inspect
import os
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from ..enums import ButtonStyle, ComponentType
from ..partial_emoji import PartialEmoji, _EmojiTag
from ..components import Button as ButtonComponent
__all__ = (
'Button',
'button',
)
if TYPE_CHECKING:
from typing_extensions import Self
from .view import BaseView
from .action_row import ActionRow
from .container import Container
from ..emoji import Emoji
from ..types.components import ButtonComponent as ButtonComponentPayload
S = TypeVar('S', bound='Union[BaseView, Container, ActionRow]', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
class Button(Item[V]):
"""Represents a UI button.
.. versionadded:: 2.0
Parameters
------------
style: :class:`discord.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, it does not have a custom ID.
Can only be up to 100 characters.
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.
Can only be up to 80 characters.
emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]]
The emoji of the button, if available.
row: Optional[:class:`int`]
The relative row this button 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).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
sku_id: Optional[:class:`int`]
The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji``
nor ``custom_id``.
.. versionadded:: 2.4
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
.. versionadded:: 2.6
"""
__item_repr_attributes__: Tuple[str, ...] = (
'style',
'url',
'disabled',
'label',
'emoji',
'row',
'sku_id',
'id',
)
def __init__(
self,
*,
style: ButtonStyle = ButtonStyle.secondary,
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
row: Optional[int] = None,
sku_id: Optional[int] = None,
id: Optional[int] = None,
):
super().__init__()
if custom_id is not None and (url is not None or sku_id is not None):
raise TypeError('cannot mix both url or sku_id and custom_id with Button')
if url is not None and sku_id is not None:
raise TypeError('cannot mix both url and sku_id')
requires_custom_id = url is None and sku_id is None
self._provided_custom_id = custom_id is not None
if requires_custom_id and custom_id is None:
custom_id = os.urandom(16).hex()
if custom_id is not None and not isinstance(custom_id, str):
raise TypeError(f'expected custom_id to be str not {custom_id.__class__.__name__}')
if url is not None:
style = ButtonStyle.link
if sku_id is not None:
style = ButtonStyle.premium
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__.__name__}')
self._underlying = ButtonComponent._raw_construct(
custom_id=custom_id,
url=url,
disabled=disabled,
label=label,
style=style,
emoji=emoji,
sku_id=sku_id,
id=id,
)
self.row = row
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this button."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
@property
def style(self) -> ButtonStyle:
""":class:`discord.ButtonStyle`: The style of the button."""
return self._underlying.style
@style.setter
def style(self, value: ButtonStyle) -> None:
self._underlying.style = value
@property
def custom_id(self) -> Optional[str]:
"""Optional[:class:`str`]: The ID of the button that gets received during an interaction.
If this button is for a URL, it does not have a custom ID.
"""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: Optional[str]) -> None:
if value is not None and not isinstance(value, str):
raise TypeError('custom_id must be None or str')
self._underlying.custom_id = value
self._provided_custom_id = value is not None
@property
def url(self) -> Optional[str]:
"""Optional[:class:`str`]: The URL this button sends you to."""
return self._underlying.url
@url.setter
def url(self, value: Optional[str]) -> None:
if value is not None and not isinstance(value, str):
raise TypeError('url must be None or str')
self._underlying.url = value
@property
def disabled(self) -> bool:
""":class:`bool`: Whether the button is disabled or not."""
return self._underlying.disabled
@disabled.setter
def disabled(self, value: bool) -> None:
self._underlying.disabled = bool(value)
@property
def label(self) -> Optional[str]:
"""Optional[:class:`str`]: The label of the button, if available."""
return self._underlying.label
@label.setter
def label(self, value: Optional[str]) -> None:
self._underlying.label = str(value) if value is not None else value
@property
def emoji(self) -> Optional[PartialEmoji]:
"""Optional[:class:`.PartialEmoji`]: The emoji of the button, if available."""
return self._underlying.emoji
@emoji.setter
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None:
if value is not None:
if isinstance(value, str):
self._underlying.emoji = PartialEmoji.from_str(value)
elif isinstance(value, _EmojiTag):
self._underlying.emoji = value._to_partial()
else:
raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__.__name__} instead')
else:
self._underlying.emoji = None
@property
def sku_id(self) -> Optional[int]:
"""Optional[:class:`int`]: The SKU ID this button sends you to.
.. versionadded:: 2.4
"""
return self._underlying.sku_id
@sku_id.setter
def sku_id(self, value: Optional[int]) -> None:
if value is not None:
self.style = ButtonStyle.premium
self._underlying.sku_id = value
@classmethod
def from_component(cls, button: ButtonComponent) -> Self:
return cls(
style=button.style,
label=button.label,
disabled=button.disabled,
custom_id=button.custom_id,
url=button.url,
emoji=button.emoji,
row=None,
sku_id=button.sku_id,
id=button.id,
)
@property
def type(self) -> Literal[ComponentType.button]:
return self._underlying.type
def to_component_dict(self) -> ButtonComponentPayload:
return self._underlying.to_dict()
def is_dispatchable(self) -> bool:
return self.custom_id is not None
def is_persistent(self) -> bool:
if self.style is ButtonStyle.link:
return self.url is not None
return super().is_persistent()
def _refresh_component(self, button: ButtonComponent) -> None:
self._underlying = button
def copy(self) -> Self:
new = copy.copy(self)
custom_id = self.custom_id
if self.custom_id is not None and not self._provided_custom_id:
custom_id = os.urandom(16).hex()
new._underlying = ButtonComponent._raw_construct(
custom_id=custom_id,
url=self.url,
disabled=self.disabled,
label=self.label,
style=self.style,
emoji=self.emoji,
sku_id=self.sku_id,
id=self.id,
)
return new
def __deepcopy__(self, memo) -> Self:
return self.copy()
def button(
*,
label: Optional[str] = None,
custom_id: Optional[str] = None,
disabled: bool = False,
style: ButtonStyle = ButtonStyle.secondary,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
row: Optional[int] = None,
id: Optional[int] = None,
) -> Callable[[ItemCallbackType[S, Button[V]]], Button[V]]:
"""A decorator that attaches a button 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.Button` being pressed.
.. note::
Buttons with a URL or an SKU cannot be created with this function.
Consider creating a :class:`Button` manually instead.
This is because these buttons cannot have a callback
associated with them since Discord does not do any processing
with them.
Parameters
------------
label: Optional[:class:`str`]
The label of the button, if any.
Can only be up to 80 characters.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
Can only be up to 100 characters.
style: :class:`.ButtonStyle`
The style of the button. Defaults to :attr:`.ButtonStyle.grey`.
disabled: :class:`bool`
Whether the button is disabled or not. Defaults to ``False``.
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
The emoji of the button. This can be in string form or a :class:`.PartialEmoji`
or a full :class:`.Emoji`.
row: Optional[:class:`int`]
The relative row this button 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).
.. note::
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
.. versionadded:: 2.6
"""
def decorator(func: ItemCallbackType[S, Button[V]]) -> ItemCallbackType[S, Button[V]]:
if not inspect.iscoroutinefunction(func):
raise TypeError('button function must be a coroutine function')
func.__discord_ui_model_type__ = Button
func.__discord_ui_model_kwargs__ = {
'style': style,
'custom_id': custom_id,
'url': None,
'disabled': disabled,
'label': label,
'emoji': emoji,
'row': row,
'sku_id': None,
'id': id,
}
return func
return decorator # type: ignore

View file

@ -0,0 +1,376 @@
"""
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
import copy
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Coroutine,
Dict,
Generator,
List,
Literal,
Optional,
TypeVar,
Union,
)
from .item import Item, ContainedItemCallbackType as ItemCallbackType
from .view import _component_to_item, LayoutView
from ..enums import ComponentType
from ..utils import get as _utils_get
from ..colour import Colour, Color
if TYPE_CHECKING:
from typing_extensions import Self
from ..components import Container as ContainerComponent
from ..interactions import Interaction
from .dynamic import DynamicItem
S = TypeVar('S', bound='Container', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Container',)
class _ContainerCallback:
__slots__ = ('container', 'callback', 'item')
def __init__(self, callback: ItemCallbackType[S, Any], container: Container, item: Item[Any]) -> None:
self.callback: ItemCallbackType[Any, Any] = callback
self.container: Container = container
self.item: Item[Any] = item
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
return self.callback(self.container, interaction, self.item)
class Container(Item[V]):
r"""Represents a UI container.
This is a top-level layout component that can only be used on :class:`LayoutView`
and can contain :class:`ActionRow`\s, :class:`TextDisplay`\s, :class:`Section`\s,
:class:`MediaGallery`\s, :class:`File`\s, and :class:`Separator`\s in it.
This can be inherited.
.. versionadded:: 2.6
Examples
--------
.. code-block:: python3
import discord
from discord import ui
# you can subclass it and add components as you would add them
# in a LayoutView
class MyContainer(ui.Container):
action_row = ui.ActionRow()
@action_row.button(label='A button in a container!')
async def a_button(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message('You clicked a button!')
# or use it directly on LayoutView
class MyView(ui.LayoutView):
container = ui.Container(ui.TextDisplay('I am a text display on a container!'))
# or you can use your subclass:
# container = MyContainer()
Parameters
----------
\*children: :class:`Item`
The initial children of this container.
accent_colour: Optional[Union[:class:`.Colour`, :class:`int`]]
The colour of the container. Defaults to ``None``.
accent_color: Optional[Union[:class:`.Colour`, :class:`int`]]
The color of the container. Defaults to ``None``.
spoiler: :class:`bool`
Whether to flag this container as a spoiler. Defaults
to ``False``.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__container_children_items__: ClassVar[Dict[str, Union[ItemCallbackType[Self, Any], Item[Any]]]] = {}
__discord_ui_container__: ClassVar[bool] = True
__item_repr_attributes__ = (
'accent_colour',
'spoiler',
'id',
)
def __init__(
self,
*children: Item[V],
accent_colour: Optional[Union[Colour, int]] = None,
accent_color: Optional[Union[Color, int]] = None,
spoiler: bool = False,
id: Optional[int] = None,
) -> None:
super().__init__()
self._children: List[Item[V]] = self._init_children()
for child in children:
self.add_item(child)
self.spoiler: bool = spoiler
self._colour = accent_colour if accent_colour is not None else accent_color
self.id = id
def __repr__(self) -> str:
return f'<{self.__class__.__name__} children={len(self._children)}>'
def _init_children(self) -> List[Item[Any]]:
children = []
parents = {}
for name, raw in self.__container_children_items__.items():
if isinstance(raw, Item):
item = raw.copy()
item._parent = self
setattr(self, name, item)
children.append(item)
parents[raw] = item
else:
# action rows can be created inside containers, and then callbacks can exist here
# so we create items based off them
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
item.callback = _ContainerCallback(raw, self, item) # type: ignore
setattr(self, raw.__name__, item)
# this should not fail because in order for a function to be here it should be from
# an action row and must have passed the check in __init_subclass__, but still
# guarding it
parent = getattr(raw, '__discord_ui_parent__', None)
if parent is None:
raise ValueError(f'{raw.__name__} is not a valid item for a Container')
parents.get(parent, parent)._children.append(item)
# we do not append it to the children list because technically these buttons and
# selects are not from the container but the action row itself.
return children
def __init_subclass__(cls) -> None:
super().__init_subclass__()
children: Dict[str, Union[ItemCallbackType[Self, Any], Item[Any]]] = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if isinstance(member, Item):
children[name] = member
if hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None):
children[name] = copy.copy(member)
cls.__container_children_items__ = children
def _update_view(self, view) -> bool:
self._view = view
for child in self._children:
child._update_view(view)
return True
def _has_children(self):
return True
def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None:
child_index = self._children.index(base)
self._children[child_index] = new # type: ignore
@property
def children(self) -> List[Item[V]]:
"""List[:class:`Item`]: The children of this container."""
return self._children.copy()
@property
def accent_colour(self) -> Optional[Union[Colour, int]]:
"""Optional[Union[:class:`discord.Colour`, :class:`int`]]: The colour of the container, or ``None``."""
return self._colour
@accent_colour.setter
def accent_colour(self, value: Optional[Union[Colour, int]]) -> None:
if value is not None and not isinstance(value, (int, Colour)):
raise TypeError(f'expected an int, or Colour, not {value.__class__.__name__!r}')
self._colour = value
accent_color = accent_colour
@property
def type(self) -> Literal[ComponentType.container]:
return ComponentType.container
@property
def width(self):
return 5
@property
def _total_count(self) -> int:
# 1 for self and all children
return 1 + len(tuple(self.walk_children()))
def _is_v2(self) -> bool:
return True
def to_components(self) -> List[Dict[str, Any]]:
components = []
for i in self._children:
components.append(i.to_component_dict())
return components
def to_component_dict(self) -> Dict[str, Any]:
components = self.to_components()
colour = None
if self._colour:
colour = self._colour if isinstance(self._colour, int) else self._colour.value
base = {
'type': self.type.value,
'accent_color': colour,
'spoiler': self.spoiler,
'components': components,
}
if self.id is not None:
base['id'] = self.id
return base
@classmethod
def from_component(cls, component: ContainerComponent) -> Self:
self = cls(
accent_colour=component.accent_colour,
spoiler=component.spoiler,
id=component.id,
)
self._children = [_component_to_item(cmp, self) for cmp in component.children]
return self
def walk_children(self) -> Generator[Item[V], None, None]:
"""An iterator that recursively walks through all the children of this container
and its children, if applicable.
Yields
------
:class:`Item`
An item in the container.
"""
for child in self.children:
yield child
if child._has_children():
yield from child.walk_children() # type: ignore
def content_length(self) -> int:
""":class:`int`: Returns the total length of all text content in this container."""
from .text_display import TextDisplay
return sum(len(item.content) for item in self.walk_children() if isinstance(item, TextDisplay))
def add_item(self, item: Item[Any]) -> Self:
"""Adds an item to this container.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to append.
Raises
------
TypeError
An :class:`Item` was not passed.
ValueError
Maximum number of children has been exceeded (40) for the entire view.
"""
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__.__name__}')
if self._view:
self._view._add_count(item._total_count)
self._children.append(item)
item._update_view(self.view)
item._parent = self
return self
def remove_item(self, item: Item[Any]) -> Self:
"""Removes an item from this container.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to remove from the container.
"""
try:
self._children.remove(item)
except ValueError:
pass
else:
if self._view:
self._view._add_count(-item._total_count)
return self
def find_item(self, id: int, /) -> Optional[Item[V]]:
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
not found.
.. warning::
This is **not the same** as ``custom_id``.
Parameters
----------
id: :class:`int`
The ID of the component.
Returns
-------
Optional[:class:`Item`]
The item found, or ``None``.
"""
return _utils_get(self.walk_children(), id=id)
def clear_items(self) -> Self:
"""Removes all the items from the container.
This function returns the class instance to allow for fluent-style
chaining.
"""
if self._view:
self._view._add_count(-len(tuple(self.walk_children())))
self._children.clear()
return self

View file

@ -0,0 +1,219 @@
"""
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 ClassVar, Dict, Generic, Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Any, Union
import re
from .item import Item
from .._types import ClientT
__all__ = ('DynamicItem',)
BaseT = TypeVar('BaseT', bound='Item[Any]', covariant=True)
if TYPE_CHECKING:
from typing_extensions import TypeVar, Self
from ..interactions import Interaction
from ..components import Component
from ..enums import ComponentType
from .view import View, LayoutView
else:
View = LayoutView = Any
class DynamicItem(Generic[BaseT], Item[Union[View, LayoutView]]):
"""Represents an item with a dynamic ``custom_id`` that can be used to store state within
that ``custom_id``.
The ``custom_id`` parsing is done using the ``re`` module by passing a ``template``
parameter to the class parameter list.
This item is generated every time the component is dispatched. This means that
any variable that holds an instance of this class will eventually be out of date
and should not be used long term. Their only purpose is to act as a "template"
for the actual dispatched item.
When this item is generated, :attr:`view` is set to a regular :class:`View` instance,
but to a :class:`LayoutView` if the component was sent with one, this is obtained from
the original message given from the interaction. This means that custom view subclasses
cannot be accessed from this item.
.. versionadded:: 2.4
Parameters
------------
item: :class:`Item`
The item to wrap with dynamic custom ID parsing.
template: Union[:class:`str`, ``re.Pattern``]
The template to use for parsing the ``custom_id``. This can be a string or a compiled
regular expression. This must be passed as a keyword argument to the class creation.
row: Optional[:class:`int`]
The relative row this button 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).
Attributes
-----------
item: :class:`Item`
The item that is wrapped with dynamic custom ID parsing.
"""
__item_repr_attributes__: Tuple[str, ...] = (
'item',
'template',
)
__discord_ui_compiled_template__: ClassVar[re.Pattern[str]]
def __init_subclass__(cls, *, template: Union[str, re.Pattern[str]]) -> None:
super().__init_subclass__()
cls.__discord_ui_compiled_template__ = re.compile(template) if isinstance(template, str) else template
if not isinstance(cls.__discord_ui_compiled_template__, re.Pattern):
raise TypeError('template must be a str or a re.Pattern')
def __init__(
self,
item: BaseT,
*,
row: Optional[int] = None,
) -> None:
super().__init__()
self.item: BaseT = item
if row is not None:
self.row = row
if not self.item.is_dispatchable():
raise TypeError('item must be dispatchable, e.g. not a URL button')
if not self.template.match(self.custom_id):
raise ValueError(f'item custom_id {self.custom_id!r} must match the template {self.template.pattern!r}')
@property
def template(self) -> re.Pattern[str]:
"""``re.Pattern``: The compiled regular expression that is used to parse the ``custom_id``."""
return self.__class__.__discord_ui_compiled_template__
def to_component_dict(self) -> Dict[str, Any]:
return self.item.to_component_dict()
def _refresh_component(self, component: Component) -> None:
self.item._refresh_component(component)
def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None:
self.item._refresh_state(interaction, data)
@classmethod
def from_component(cls: Type[Self], component: Component) -> Self:
raise TypeError('Dynamic items cannot be created from components')
@property
def type(self) -> ComponentType:
return self.item.type
def is_dispatchable(self) -> bool:
return self.item.is_dispatchable()
def is_persistent(self) -> bool:
return True
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the dynamic item that gets received during an interaction."""
return self.item.custom_id # type: ignore # This attribute exists for dispatchable items
@custom_id.setter
def custom_id(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError('custom_id must be a str')
if not self.template.match(value):
raise ValueError(f'custom_id must match the template {self.template.pattern!r}')
self.item.custom_id = value # type: ignore # This attribute exists for dispatchable items
self._provided_custom_id = True
@property
def row(self) -> Optional[int]:
return self.item._row
@row.setter
def row(self, value: Optional[int]) -> None:
self.item.row = value
@property
def width(self) -> int:
return self.item.width
@property
def _total_count(self) -> int:
return self.item._total_count
@classmethod
async def from_custom_id(
cls: Type[Self], interaction: Interaction[ClientT], item: Item[Any], match: re.Match[str], /
) -> Self:
"""|coro|
A classmethod that is called when the ``custom_id`` of a component matches the
``template`` of the class. This is called when the component is dispatched.
It must return a new instance of the :class:`DynamicItem`.
Subclasses *must* implement this method.
Exceptions raised in this method are logged and ignored.
.. warning::
This method is called before the callback is dispatched, therefore
it means that it is subject to the same timing restrictions as the callback.
Ergo, you must reply to an interaction within 3 seconds of it being
dispatched.
Parameters
------------
interaction: :class:`~discord.Interaction`
The interaction that the component belongs to.
item: :class:`~discord.ui.Item`
The base item that is being dispatched.
match: ``re.Match``
The match object that was created from the ``template``
matching the ``custom_id``.
Returns
--------
:class:`DynamicItem`
The new instance of the :class:`DynamicItem` with information
from the ``match`` object.
"""
raise NotImplementedError
async def callback(self, interaction: Interaction[ClientT]) -> Any:
return await self.item.callback(interaction)
async def interaction_check(self, interaction: Interaction[ClientT], /) -> bool:
return await self.item.interaction_check(interaction)

View file

@ -0,0 +1,159 @@
"""
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 TYPE_CHECKING, Literal, Optional, TypeVar, Union
from .item import Item
from ..components import FileComponent, UnfurledMediaItem
from ..enums import ComponentType
from ..utils import MISSING
from ..file import File as SendableFile
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('File',)
class File(Item[V]):
"""Represents a UI file component.
This is a top-level layout component that can only be used on :class:`LayoutView`.
.. versionadded:: 2.6
Example
-------
.. code-block:: python3
import discord
from discord import ui
class MyView(ui.LayoutView):
file = ui.File('attachment://file.txt')
# attachment://file.txt points to an attachment uploaded alongside this view
Parameters
----------
media: Union[:class:`str`, :class:`.UnfurledMediaItem`, :class:`discord.File`]
This file's media. If this is a string it must point to a local
file uploaded within the parent view of this item, and must
meet the ``attachment://<filename>`` format.
spoiler: :class:`bool`
Whether to flag this file as a spoiler. Defaults to ``False``.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__item_repr_attributes__ = (
'media',
'spoiler',
'id',
)
def __init__(
self,
media: Union[str, UnfurledMediaItem, SendableFile],
*,
spoiler: bool = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__()
if isinstance(media, SendableFile):
self._underlying = FileComponent._raw_construct(
media=UnfurledMediaItem(media.uri),
spoiler=media.spoiler if spoiler is MISSING else spoiler,
id=id,
)
else:
self._underlying = FileComponent._raw_construct(
media=UnfurledMediaItem(media) if isinstance(media, str) else media,
spoiler=bool(spoiler),
id=id,
)
self.id = id
def _is_v2(self):
return True
@property
def width(self):
return 5
@property
def type(self) -> Literal[ComponentType.file]:
return self._underlying.type
@property
def media(self) -> UnfurledMediaItem:
""":class:`.UnfurledMediaItem`: Returns this file media."""
return self._underlying.media
@media.setter
def media(self, value: Union[str, SendableFile, UnfurledMediaItem]) -> None:
if isinstance(value, str):
self._underlying.media = UnfurledMediaItem(value)
elif isinstance(value, UnfurledMediaItem):
self._underlying.media = value
elif isinstance(value, SendableFile):
self._underlying.media = UnfurledMediaItem(value.uri)
else:
raise TypeError(f'expected a str or UnfurledMediaItem or File, not {value.__class__.__name__!r}')
@property
def url(self) -> str:
""":class:`str`: Returns this file's url."""
return self._underlying.media.url
@url.setter
def url(self, value: str) -> None:
self._underlying.media = UnfurledMediaItem(value)
@property
def spoiler(self) -> bool:
""":class:`bool`: Returns whether this file should be flagged as a spoiler."""
return self._underlying.spoiler
@spoiler.setter
def spoiler(self, value: bool) -> None:
self._underlying.spoiler = value
def to_component_dict(self):
return self._underlying.to_dict()
@classmethod
def from_component(cls, component: FileComponent) -> Self:
return cls(
media=component.media,
spoiler=component.spoiler,
id=component.id,
)

View file

@ -0,0 +1,243 @@
"""
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
import copy
from typing import Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Union, Tuple, Type, TypeVar
from ..interactions import Interaction
from .._types import ClientT
# fmt: off
__all__ = (
'Item',
)
# fmt: on
if TYPE_CHECKING:
from typing_extensions import Self
from ..enums import ComponentType
from .view import BaseView
from ..components import Component
from .action_row import ActionRow
from .container import Container
from .dynamic import DynamicItem
from ..app_commands.namespace import ResolveKey
I = TypeVar('I', bound='Item[Any]')
V = TypeVar('V', bound='BaseView', covariant=True)
ContainerType = Union['BaseView', 'ActionRow', 'Container']
C = TypeVar('C', bound=ContainerType, covariant=True)
ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]]
ContainedItemCallbackType = Callable[[C, Interaction[Any], I], Coroutine[Any, Any, Any]]
class Item(Generic[V]):
"""Represents the base UI item that all UI components inherit from.
The current UI items supported are:
- :class:`discord.ui.Button`
- :class:`discord.ui.Select`
- :class:`discord.ui.TextInput`
- :class:`discord.ui.ActionRow`
- :class:`discord.ui.Container`
- :class:`discord.ui.File`
- :class:`discord.ui.MediaGallery`
- :class:`discord.ui.Section`
- :class:`discord.ui.Separator`
- :class:`discord.ui.TextDisplay`
- :class:`discord.ui.Thumbnail`
- :class:`discord.ui.Label`
.. versionadded:: 2.0
"""
__item_repr_attributes__: Tuple[str, ...] = ('row', 'id')
def __init__(self):
self._view: Optional[V] = None
self._row: Optional[int] = None
self._rendered_row: Optional[int] = None
# This works mostly well but there is a gotcha with
# the interaction with from_component, since that technically provides
# a custom_id most dispatchable items would get this set to True even though
# it might not be provided by the library user. However, this edge case doesn't
# actually affect the intended purpose of this check because from_component is
# only called upon edit and we're mainly interested during initial creation time.
self._provided_custom_id: bool = False
self._id: Optional[int] = None
self._parent: Optional[Item] = None
def to_component_dict(self) -> Dict[str, Any]:
raise NotImplementedError
def _refresh_component(self, component: Component) -> None:
return None
def _handle_submit(self, interaction: Interaction, data: Dict[str, Any], resolved: Dict[ResolveKey, Any]) -> None:
return self._refresh_state(interaction, data)
def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None:
return None
def _is_v2(self) -> bool:
return False
@classmethod
def from_component(cls: Type[I], component: Component) -> I:
return cls()
@property
def type(self) -> ComponentType:
raise NotImplementedError
def is_dispatchable(self) -> bool:
return False
def is_persistent(self) -> bool:
if self.is_dispatchable():
return self._provided_custom_id
return True
def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None:
raise ValueError
def __repr__(self) -> str:
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__)
return f'<{self.__class__.__name__} {attrs}>'
@property
def row(self) -> Optional[int]:
return self._row
@row.setter
def row(self, value: Optional[int]) -> None:
if self._is_v2():
# row is ignored on v2 components
return
if value is None:
self._row = None
elif 5 > value >= 0:
self._row = value
else:
raise ValueError('row cannot be negative or greater than or equal to 5')
@property
def width(self) -> int:
return 1
@property
def _total_count(self) -> int:
return 1
@property
def view(self) -> Optional[V]:
"""Optional[Union[:class:`View`, :class:`LayoutView`]]: The underlying view for this item."""
return self._view
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this component."""
return self._id
@id.setter
def id(self, value: Optional[int]) -> None:
self._id = value
@property
def parent(self) -> Optional[Item[V]]:
"""Optional[:class:`Item`]: This item's parent, if applicable. Only available on items with children.
.. versionadded:: 2.6
"""
return self._parent
async def _run_checks(self, interaction: Interaction[ClientT]) -> bool:
can_run = await self.interaction_check(interaction)
if can_run and self._parent:
can_run = await self._parent._run_checks(interaction)
return can_run
def _update_view(self, view) -> None:
self._view = view
def copy(self) -> Self:
return copy.deepcopy(self)
def _has_children(self) -> bool:
return False
async def callback(self, interaction: Interaction[ClientT]) -> Any:
"""|coro|
The callback associated with this UI item.
This can be overridden by subclasses.
Parameters
-----------
interaction: :class:`.Interaction`
The interaction that triggered this UI item.
"""
pass
async def interaction_check(self, interaction: Interaction[ClientT], /) -> bool:
"""|coro|
A callback that is called when an interaction happens within this item
that checks whether the callback should be processed.
This is useful to override if, for example, you want to ensure that the
interaction author is a given user.
The default implementation of this returns ``True``.
.. note::
If an exception occurs within the body then the check
is considered a failure and :meth:`View.on_error`
(or :meth:`LayoutView.on_error`) is called.
For :class:`~discord.ui.DynamicItem` this does not call the ``on_error``
handler.
.. versionadded:: 2.4
Parameters
-----------
interaction: :class:`~discord.Interaction`
The interaction that occurred.
Returns
---------
:class:`bool`
Whether the callback should be called.
"""
return True

View file

@ -0,0 +1,141 @@
"""
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 TYPE_CHECKING, Generator, Literal, Optional, Tuple, TypeVar
from ..components import LabelComponent
from ..enums import ComponentType
from ..utils import MISSING
from .item import Item
if TYPE_CHECKING:
from typing_extensions import Self
from ..types.components import LabelComponent as LabelComponentPayload
from .view import BaseView
# fmt: off
__all__ = (
'Label',
)
# fmt: on
V = TypeVar('V', bound='BaseView', covariant=True)
class Label(Item[V]):
"""Represents a UI label within a modal.
This is a top-level layout component that can only be used on :class:`Modal`.
.. versionadded:: 2.6
Parameters
------------
text: :class:`str`
The text to display above the input field.
Can only be up to 45 characters.
description: Optional[:class:`str`]
The description text to display right below the label text.
Can only be up to 100 characters.
component: :class:`Item`
The component to display below the label.
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
Attributes
------------
text: :class:`str`
The text to display above the input field.
Can only be up to 45 characters.
description: Optional[:class:`str`]
The description text to display right below the label text.
Can only be up to 100 characters.
component: :class:`Item`
The component to display below the label.
"""
__item_repr_attributes__: Tuple[str, ...] = (
'text',
'description',
'component',
)
def __init__(
self,
*,
text: str,
component: Item[V],
description: Optional[str] = None,
id: Optional[int] = None,
) -> None:
super().__init__()
self.component: Item[V] = component
self.text: str = text
self.description: Optional[str] = description
self.id = id
@property
def width(self) -> int:
return 5
def _has_children(self) -> bool:
return True
def walk_children(self) -> Generator[Item[V], None, None]:
yield self.component
def to_component_dict(self) -> LabelComponentPayload:
payload: LabelComponentPayload = {
'type': ComponentType.label.value,
'label': self.text,
'component': self.component.to_component_dict(), # type: ignore
}
if self.description:
payload['description'] = self.description
if self.id is not None:
payload['id'] = self.id
return payload
@classmethod
def from_component(cls, component: LabelComponent) -> Self:
from .view import _component_to_item
self = cls(
text=component.label,
component=MISSING,
description=component.description,
)
self.component = _component_to_item(component.component, self)
return self
@property
def type(self) -> Literal[ComponentType.label]:
return ComponentType.label
def is_dispatchable(self) -> bool:
return False

View file

@ -0,0 +1,263 @@
"""
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 TYPE_CHECKING, List, Literal, Optional, TypeVar, Union
from .item import Item
from ..enums import ComponentType
from ..utils import MISSING
from ..file import File
from ..components import (
MediaGalleryItem,
MediaGalleryComponent,
UnfurledMediaItem,
)
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('MediaGallery',)
class MediaGallery(Item[V]):
r"""Represents a UI media gallery.
Can contain up to 10 :class:`.MediaGalleryItem`\s.
This is a top-level layout component that can only be used on :class:`LayoutView`.
.. versionadded:: 2.6
Parameters
----------
\*items: :class:`.MediaGalleryItem`
The initial items of this gallery.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__item_repr_attributes__ = (
'items',
'id',
)
def __init__(
self,
*items: MediaGalleryItem,
id: Optional[int] = None,
) -> None:
super().__init__()
self._underlying = MediaGalleryComponent._raw_construct(
items=list(items),
id=id,
)
def __repr__(self) -> str:
return f'<{self.__class__.__name__} items={len(self._underlying.items)}>'
@property
def items(self) -> List[MediaGalleryItem]:
"""List[:class:`.MediaGalleryItem`]: Returns a read-only list of this gallery's items."""
return self._underlying.items.copy()
@items.setter
def items(self, value: List[MediaGalleryItem]) -> None:
if len(value) > 10:
raise ValueError('media gallery only accepts up to 10 items')
self._underlying.items = value
@property
def id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of this component."""
return self._underlying.id
@id.setter
def id(self, value: Optional[int]) -> None:
self._underlying.id = value
def to_component_dict(self):
return self._underlying.to_dict()
def _is_v2(self) -> bool:
return True
def add_item(
self,
*,
media: Union[str, File, UnfurledMediaItem],
description: Optional[str] = MISSING,
spoiler: bool = MISSING,
) -> Self:
"""Adds an item to this gallery.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
media: Union[:class:`str`, :class:`discord.File`, :class:`.UnfurledMediaItem`]
The media item data. This can be a string representing a local
file uploaded as an attachment in the message, which can be accessed
using the ``attachment://<filename>`` format, or an arbitrary url.
description: Optional[:class:`str`]
The description to show within this item. Up to 256 characters. Defaults
to ``None``.
spoiler: :class:`bool`
Whether this item should be flagged as a spoiler. Defaults to ``False``.
Raises
------
ValueError
Maximum number of items has been exceeded (10).
"""
if len(self._underlying.items) >= 10:
raise ValueError('maximum number of items has been exceeded')
item = MediaGalleryItem(media, description=description, spoiler=spoiler)
self._underlying.items.append(item)
return self
def append_item(self, item: MediaGalleryItem) -> Self:
"""Appends an item to this gallery.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`.MediaGalleryItem`
The item to add to the gallery.
Raises
------
TypeError
A :class:`.MediaGalleryItem` was not passed.
ValueError
Maximum number of items has been exceeded (10).
"""
if len(self._underlying.items) >= 10:
raise ValueError('maximum number of items has been exceeded')
if not isinstance(item, MediaGalleryItem):
raise TypeError(f'expected MediaGalleryItem, not {item.__class__.__name__!r}')
self._underlying.items.append(item)
return self
def insert_item_at(
self,
index: int,
*,
media: Union[str, File, UnfurledMediaItem],
description: Optional[str] = MISSING,
spoiler: bool = MISSING,
) -> Self:
"""Inserts an item before a specified index to the media gallery.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
index: :class:`int`
The index of where to insert the field.
media: Union[:class:`str`, :class:`discord.File`, :class:`.UnfurledMediaItem`]
The media item data. This can be a string representing a local
file uploaded as an attachment in the message, which can be accessed
using the ``attachment://<filename>`` format, or an arbitrary url.
description: Optional[:class:`str`]
The description to show within this item. Up to 256 characters. Defaults
to ``None``.
spoiler: :class:`bool`
Whether this item should be flagged as a spoiler. Defaults to ``False``.
Raises
------
ValueError
Maximum number of items has been exceeded (10).
"""
if len(self._underlying.items) >= 10:
raise ValueError('maximum number of items has been exceeded')
item = MediaGalleryItem(
media,
description=description,
spoiler=spoiler,
)
self._underlying.items.insert(index, item)
return self
def remove_item(self, item: MediaGalleryItem) -> Self:
"""Removes an item from the gallery.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`.MediaGalleryItem`
The item to remove from the gallery.
"""
try:
self._underlying.items.remove(item)
except ValueError:
pass
return self
def clear_items(self) -> Self:
"""Removes all items from the gallery.
This function returns the class instance to allow for fluent-style
chaining.
"""
self._underlying.items.clear()
return self
@property
def type(self) -> Literal[ComponentType.media_gallery]:
return self._underlying.type
@property
def width(self):
return 5
@classmethod
def from_component(cls, component: MediaGalleryComponent) -> Self:
return cls(
*component.items,
id=component.id,
)

View file

@ -0,0 +1,273 @@
"""
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
import asyncio
import logging
import os
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, ClassVar, List
from ..utils import MISSING, find
from .._types import ClientT
from .item import Item
from .view import BaseView
from .select import BaseSelect
from .text_input import TextInput
from ..interactions import Namespace
if TYPE_CHECKING:
from typing_extensions import Self
from ..interactions import Interaction
from ..types.interactions import (
ModalSubmitComponentInteractionData as ModalSubmitComponentInteractionDataPayload,
ResolvedData as ResolvedDataPayload,
)
from ..app_commands.namespace import ResolveKey
# fmt: off
__all__ = (
'Modal',
)
# fmt: on
_log = logging.getLogger(__name__)
class Modal(BaseView):
"""Represents a UI modal.
This object must be inherited to create a modal popup window within discord.
.. versionadded:: 2.0
Examples
----------
.. code-block:: python3
import discord
from discord import ui
class Questionnaire(ui.Modal, title='Questionnaire Response'):
name = ui.TextInput(label='Name')
answer = ui.TextInput(label='Answer', style=discord.TextStyle.paragraph)
async def on_submit(self, interaction: discord.Interaction):
await interaction.response.send_message(f'Thanks for your response, {self.name}!', ephemeral=True)
Parameters
-----------
title: :class:`str`
The title of the modal.
Can only be up to 45 characters.
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the UI before no longer accepting input.
If ``None`` then there is no timeout.
custom_id: :class:`str`
The ID of the modal that gets received during an interaction.
If not given then one is generated for you.
Can only be up to 100 characters.
Attributes
------------
title: :class:`str`
The title of the modal.
custom_id: :class:`str`
The ID of the modal that gets received during an interaction.
"""
if TYPE_CHECKING:
title: str
__discord_ui_modal__ = True
__modal_children_items__: ClassVar[Dict[str, Item[Self]]] = {}
def __init_subclass__(cls, *, title: str = MISSING) -> None:
if title is not MISSING:
cls.title = title
children = {}
for base in reversed(cls.__mro__):
for name, member in base.__dict__.items():
if isinstance(member, Item):
children[name] = member
cls.__modal_children_items__ = children
def _init_children(self) -> List[Item]:
children = []
for name, item in self.__modal_children_items__.items():
item = deepcopy(item)
setattr(self, name, item)
item._view = self
children.append(item)
return children
def __init__(
self,
*,
title: str = MISSING,
timeout: Optional[float] = None,
custom_id: str = MISSING,
) -> None:
if title is MISSING and getattr(self, 'title', MISSING) is MISSING:
raise ValueError('Modal must have a title')
elif title is not MISSING:
self.title = title
self.custom_id: str = os.urandom(16).hex() if custom_id is MISSING else custom_id
super().__init__(timeout=timeout)
async def on_submit(self, interaction: Interaction[ClientT], /) -> None:
"""|coro|
Called when the modal is submitted.
Parameters
-----------
interaction: :class:`.Interaction`
The interaction that submitted this modal.
"""
pass
async def on_error(self, interaction: Interaction[ClientT], error: Exception, /) -> None:
"""|coro|
A callback that is called when :meth:`on_submit`
fails with an error.
The default implementation logs to the library logger.
Parameters
-----------
interaction: :class:`~discord.Interaction`
The interaction that led to the failure.
error: :class:`Exception`
The exception that was raised.
"""
_log.error('Ignoring exception in modal %r:', self, exc_info=error)
def _refresh(
self,
interaction: Interaction,
components: Sequence[ModalSubmitComponentInteractionDataPayload],
resolved: Dict[ResolveKey, Any],
) -> None:
for component in components:
if component['type'] == 1:
self._refresh(interaction, component['components'], resolved) # type: ignore
elif component['type'] == 18:
self._refresh(interaction, [component['component']], resolved) # type: ignore
else:
custom_id = component.get('custom_id')
if custom_id is None:
continue
item = find(
lambda i: getattr(i, 'custom_id', None) == custom_id,
self.walk_children(),
)
if item is None:
_log.debug('Modal interaction referencing unknown item custom_id %s. Discarding', custom_id)
continue
item._handle_submit(interaction, component, resolved) # type: ignore
async def _scheduled_task(
self,
interaction: Interaction,
components: List[ModalSubmitComponentInteractionDataPayload],
resolved: Dict[ResolveKey, Any],
):
try:
self._refresh_timeout()
self._refresh(interaction, components, resolved)
allow = await self.interaction_check(interaction)
if not allow:
return
await self.on_submit(interaction)
except Exception as e:
return await self.on_error(interaction, e)
else:
# No error, so assume this will always happen
# In the future, maybe this will require checking if we set an error response.
self.stop()
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
return item._rendered_row or 0
children = sorted(self._children, key=key)
components: List[Dict[str, Any]] = []
for child in children:
if isinstance(child, (BaseSelect, TextInput)):
# Every implicit child wrapped in an ActionRow in a modal
# has a single child of width 5
# It's also deprecated to use ActionRow in modals
components.append(
{
'type': 1,
'components': [child.to_component_dict()],
}
)
else:
components.append(child.to_component_dict())
return components
def _dispatch_submit(
self,
interaction: Interaction,
components: List[ModalSubmitComponentInteractionDataPayload],
resolved: ResolvedDataPayload,
) -> asyncio.Task[None]:
try:
namespace = Namespace._get_resolved_items(interaction, resolved)
except KeyError:
namespace = {}
return asyncio.create_task(
self._scheduled_task(interaction, components, namespace), name=f'discord-ui-modal-dispatch-{self.id}'
)
def to_dict(self) -> Dict[str, Any]:
payload = {
'custom_id': self.custom_id,
'title': self.title,
'components': self.to_components(),
}
return payload
def add_item(self, item: Item[Any]) -> Self:
if len(self._children) >= 5:
raise ValueError('maximum number of children exceeded (5)')
return super().add_item(item)

View file

@ -0,0 +1,277 @@
"""
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 TYPE_CHECKING, Any, Dict, Generator, List, Literal, Optional, TypeVar, Union, ClassVar
from .item import Item
from .text_display import TextDisplay
from ..enums import ComponentType
from ..utils import get as _utils_get
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
from .dynamic import DynamicItem
from ..components import SectionComponent
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Section',)
class Section(Item[V]):
r"""Represents a UI section.
This is a top-level layout component that can only be used on :class:`LayoutView`.
.. versionadded:: 2.6
Parameters
----------
\*children: Union[:class:`str`, :class:`TextDisplay`]
The text displays of this section. Up to 3.
accessory: :class:`Item`
The section accessory. This is usually either a :class:`Button` or :class:`Thumbnail`.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__item_repr_attributes__ = (
'accessory',
'id',
)
__discord_ui_section__: ClassVar[bool] = True
__slots__ = (
'_children',
'_accessory',
)
def __init__(
self,
*children: Union[Item[V], str],
accessory: Item[V],
id: Optional[int] = None,
) -> None:
super().__init__()
self._children: List[Item[V]] = []
for child in children:
self.add_item(child)
accessory._parent = self
self._accessory: Item[V] = accessory
self.id = id
def __repr__(self) -> str:
return f'<{self.__class__.__name__} children={len(self._children)} accessory={self._accessory!r}>'
@property
def type(self) -> Literal[ComponentType.section]:
return ComponentType.section
@property
def children(self) -> List[Item[V]]:
"""List[:class:`Item`]: The list of children attached to this section."""
return self._children.copy()
@property
def width(self):
return 5
@property
def _total_count(self) -> int:
# Count the accessory, ourselves, and all children
return 2 + len(self._children)
@property
def accessory(self) -> Item[V]:
""":class:`Item`: The section's accessory."""
return self._accessory
@accessory.setter
def accessory(self, value: Item[V]) -> None:
if not isinstance(value, Item):
raise TypeError(f'Expected an Item, got {value.__class__.__name__!r} instead')
value._update_view(self.view)
value._parent = self
self._accessory = value
def _is_v2(self) -> bool:
return True
def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None:
if self.accessory.is_dispatchable() and getattr(self.accessory, 'custom_id', None) == custom_id:
self.accessory = new # type: ignore
def walk_children(self) -> Generator[Item[V], None, None]:
"""An iterator that recursively walks through all the children of this section
and its children, if applicable. This includes the `accessory`.
Yields
------
:class:`Item`
An item in this section.
"""
for child in self.children:
yield child
yield self.accessory
def _update_view(self, view) -> None:
self._view = view
self.accessory._view = view
for child in self._children:
child._view = view
def _has_children(self):
return True
def content_length(self) -> int:
""":class:`int`: Returns the total length of all text content in this section."""
from .text_display import TextDisplay
return sum(len(item.content) for item in self._children if isinstance(item, TextDisplay))
def add_item(self, item: Union[str, Item[Any]]) -> Self:
"""Adds an item to this section.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: Union[:class:`str`, :class:`Item`]
The item to append, if it is a string it automatically wrapped around
:class:`TextDisplay`.
Raises
------
TypeError
An :class:`Item` or :class:`str` was not passed.
ValueError
Maximum number of children has been exceeded (3) or (40)
for the entire view.
"""
if len(self._children) >= 3:
raise ValueError('maximum number of children exceeded (3)')
if not isinstance(item, (Item, str)):
raise TypeError(f'expected Item or str not {item.__class__.__name__}')
if self._view:
self._view._add_count(1)
item = item if isinstance(item, Item) else TextDisplay(item)
item._update_view(self.view)
item._parent = self
self._children.append(item)
return self
def remove_item(self, item: Item[Any]) -> Self:
"""Removes an item from this section.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
----------
item: :class:`Item`
The item to remove from the section.
"""
try:
self._children.remove(item)
except ValueError:
pass
else:
if self._view:
self._view._add_count(-1)
return self
def find_item(self, id: int, /) -> Optional[Item[V]]:
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
not found.
.. warning::
This is **not the same** as ``custom_id``.
Parameters
----------
id: :class:`int`
The ID of the component.
Returns
-------
Optional[:class:`Item`]
The item found, or ``None``.
"""
return _utils_get(self.walk_children(), id=id)
def clear_items(self) -> Self:
"""Removes all the items from the section.
This function returns the class instance to allow for fluent-style
chaining.
"""
if self._view:
self._view._add_count(-len(self._children)) # we don't count the accessory because it is required
self._children.clear()
return self
@classmethod
def from_component(cls, component: SectionComponent) -> Self:
from .view import _component_to_item
accessory = _component_to_item(component.accessory, None)
self = cls(id=component.id, accessory=accessory)
self.id = component.id
self._children = [_component_to_item(c, self) for c in component.children]
return self
def to_components(self) -> List[Dict[str, Any]]:
components = []
for component in self._children:
components.append(component.to_component_dict())
return components
def to_component_dict(self) -> Dict[str, Any]:
data = {
'type': self.type.value,
'components': self.to_components(),
'accessory': self.accessory.to_component_dict(),
}
if self.id is not None:
data['id'] = self.id
return data

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,125 @@
"""
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 TYPE_CHECKING, Literal, Optional, TypeVar
from .item import Item
from ..components import SeparatorComponent
from ..enums import SeparatorSpacing, ComponentType
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Separator',)
class Separator(Item[V]):
"""Represents a UI separator.
This is a top-level layout component that can only be used on :class:`LayoutView`.
.. versionadded:: 2.6
Parameters
----------
visible: :class:`bool`
Whether this separator is visible. On the client side this
is whether a divider line should be shown or not.
spacing: :class:`.SeparatorSpacing`
The spacing of this separator.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__slots__ = ('_underlying',)
__item_repr_attributes__ = (
'visible',
'spacing',
'id',
)
def __init__(
self,
*,
visible: bool = True,
spacing: SeparatorSpacing = SeparatorSpacing.small,
id: Optional[int] = None,
) -> None:
super().__init__()
self._underlying = SeparatorComponent._raw_construct(
spacing=spacing,
visible=visible,
id=id,
)
self.id = id
def _is_v2(self):
return True
@property
def visible(self) -> bool:
""":class:`bool`: Whether this separator is visible.
On the client side this is whether a divider line should
be shown or not.
"""
return self._underlying.visible
@visible.setter
def visible(self, value: bool) -> None:
self._underlying.visible = value
@property
def spacing(self) -> SeparatorSpacing:
""":class:`.SeparatorSpacing`: The spacing of this separator."""
return self._underlying.spacing
@spacing.setter
def spacing(self, value: SeparatorSpacing) -> None:
self._underlying.spacing = value
@property
def width(self):
return 5
@property
def type(self) -> Literal[ComponentType.separator]:
return self._underlying.type
def to_component_dict(self):
return self._underlying.to_dict()
@classmethod
def from_component(cls, component: SeparatorComponent) -> Self:
return cls(
visible=component.visible,
spacing=component.spacing,
id=component.id,
)

View file

@ -0,0 +1,91 @@
"""
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 TYPE_CHECKING, Literal, Optional, TypeVar
from .item import Item
from ..components import TextDisplay as TextDisplayComponent
from ..enums import ComponentType
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('TextDisplay',)
class TextDisplay(Item[V]):
"""Represents a UI text display.
This is a top-level layout component that can only be used on :class:`LayoutView`,
:class:`Section`, :class:`Container`, or :class:`Modal`.
.. versionadded:: 2.6
Parameters
----------
content: :class:`str`
The content of this text display. Up to 4000 characters.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__slots__ = ('content',)
def __init__(self, content: str, *, id: Optional[int] = None) -> None:
super().__init__()
self.content: str = content
self.id = id
def to_component_dict(self):
base = {
'type': self.type.value,
'content': self.content,
}
if self.id is not None:
base['id'] = self.id
return base
@property
def width(self):
return 5
@property
def type(self) -> Literal[ComponentType.text_display]:
return ComponentType.text_display
def _is_v2(self) -> bool:
return True
@classmethod
def from_component(cls, component: TextDisplayComponent) -> Self:
return cls(
content=component.content,
id=component.id,
)

View file

@ -0,0 +1,270 @@
"""
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
import os
from typing import TYPE_CHECKING, Literal, Optional, Tuple, TypeVar
from ..components import TextInput as TextInputComponent
from ..enums import ComponentType, TextStyle
from ..utils import MISSING, deprecated
from .item import Item
if TYPE_CHECKING:
from typing_extensions import Self
from ..types.components import TextInput as TextInputPayload
from ..types.interactions import ModalSubmitTextInputInteractionData as ModalSubmitTextInputInteractionDataPayload
from .view import BaseView
from ..interactions import Interaction
# fmt: off
__all__ = (
'TextInput',
)
# fmt: on
V = TypeVar('V', bound='BaseView', covariant=True)
class TextInput(Item[V]):
"""Represents a UI text input.
This a top-level layout component that can only be used in :class:`Label`.
.. container:: operations
.. describe:: str(x)
Returns the value of the text input or an empty string if the value is ``None``.
.. versionadded:: 2.0
Parameters
------------
label: Optional[:class:`str`]
The label to display above the text input.
Can only be up to 45 characters.
.. deprecated:: 2.6
This parameter is deprecated, use :class:`discord.ui.Label` instead.
.. versionchanged:: 2.6
This parameter is now optional and defaults to ``None``.
custom_id: :class:`str`
The ID of the text input that gets received during an interaction.
If not given then one is generated for you.
Can only be up to 100 characters.
style: :class:`discord.TextStyle`
The style of the text input.
placeholder: Optional[:class:`str`]
The placeholder text to display when the text input is empty.
Can only be up to 100 characters.
default: Optional[:class:`str`]
The default value of the text input.
Can only be up to 4000 characters.
required: :class:`bool`
Whether the text input is required.
min_length: Optional[:class:`int`]
The minimum length of the text input.
Must be between 0 and 4000.
max_length: Optional[:class:`int`]
The maximum length of the text input.
Must be between 1 and 4000.
row: Optional[:class:`int`]
The relative row this text input 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).
id: Optional[:class:`int`]
The ID of the component. This must be unique across the view.
.. versionadded:: 2.6
"""
__item_repr_attributes__: Tuple[str, ...] = (
'label',
'placeholder',
'required',
'id',
)
def __init__(
self,
*,
label: Optional[str] = None,
style: TextStyle = TextStyle.short,
custom_id: str = MISSING,
placeholder: Optional[str] = None,
default: Optional[str] = None,
required: bool = True,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
row: Optional[int] = None,
id: Optional[int] = None,
) -> None:
super().__init__()
self._value: Optional[str] = default
self._provided_custom_id = custom_id is not MISSING
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
if not isinstance(custom_id, str):
raise TypeError(f'expected custom_id to be str not {custom_id.__class__.__name__}')
self._underlying = TextInputComponent._raw_construct(
label=label,
style=style,
custom_id=custom_id,
placeholder=placeholder,
value=default,
required=required,
min_length=min_length,
max_length=max_length,
id=id,
)
self.row = row
self.id = id
def __str__(self) -> str:
return self.value
@property
def custom_id(self) -> str:
""":class:`str`: The ID of the text input that gets received during an interaction."""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError('custom_id must be a str')
self._underlying.custom_id = value
self._provided_custom_id = True
@property
def width(self) -> int:
return 5
@property
def value(self) -> str:
""":class:`str`: The value of the text input."""
return self._value or ''
@property
@deprecated('discord.ui.Label')
def label(self) -> Optional[str]:
""":class:`str`: The label of the text input."""
return self._underlying.label
@label.setter
@deprecated('discord.ui.Label')
def label(self, value: Optional[str]) -> None:
self._underlying.label = value
@property
def placeholder(self) -> Optional[str]:
""":class:`str`: The placeholder text to display when the text input is empty."""
return self._underlying.placeholder
@placeholder.setter
def placeholder(self, value: Optional[str]) -> None:
self._underlying.placeholder = value
@property
def required(self) -> bool:
""":class:`bool`: Whether the text input is required."""
return self._underlying.required
@required.setter
def required(self, value: bool) -> None:
self._underlying.required = value
@property
def min_length(self) -> Optional[int]:
""":class:`int`: The minimum length of the text input."""
return self._underlying.min_length
@min_length.setter
def min_length(self, value: Optional[int]) -> None:
self._underlying.min_length = value
@property
def max_length(self) -> Optional[int]:
""":class:`int`: The maximum length of the text input."""
return self._underlying.max_length
@max_length.setter
def max_length(self, value: Optional[int]) -> None:
self._underlying.max_length = value
@property
def style(self) -> TextStyle:
""":class:`discord.TextStyle`: The style of the text input."""
return self._underlying.style
@style.setter
def style(self, value: TextStyle) -> None:
self._underlying.style = value
@property
def default(self) -> Optional[str]:
""":class:`str`: The default value of the text input."""
return self._underlying.value
@default.setter
def default(self, value: Optional[str]) -> None:
self._underlying.value = value
def to_component_dict(self) -> TextInputPayload:
return self._underlying.to_dict()
def _refresh_component(self, component: TextInputComponent) -> None:
self._underlying = component
def _refresh_state(self, interaction: Interaction, data: ModalSubmitTextInputInteractionDataPayload) -> None:
self._value = data.get('value', None)
@classmethod
def from_component(cls, component: TextInputComponent) -> Self:
return cls(
label=component.label,
style=component.style,
custom_id=component.custom_id,
placeholder=component.placeholder,
default=component.value,
required=component.required,
min_length=component.min_length,
max_length=component.max_length,
row=None,
id=component.id,
)
@property
def type(self) -> Literal[ComponentType.text_input]:
return self._underlying.type
def is_dispatchable(self) -> bool:
return False

View file

@ -0,0 +1,144 @@
"""
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 TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar, Union
from .item import Item
from ..enums import ComponentType
from ..components import UnfurledMediaItem
from ..file import File
from ..utils import MISSING
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
from ..components import ThumbnailComponent
V = TypeVar('V', bound='LayoutView', covariant=True)
__all__ = ('Thumbnail',)
class Thumbnail(Item[V]):
"""Represents a UI Thumbnail. This currently can only be used as a :class:`Section`\'s accessory.
.. versionadded:: 2.6
Parameters
----------
media: Union[:class:`str`, :class:`discord.File`, :class:`discord.UnfurledMediaItem`]
The media of the thumbnail. This can be a URL or a reference
to an attachment that matches the ``attachment://filename.extension``
structure.
description: Optional[:class:`str`]
The description of this thumbnail. Up to 256 characters. Defaults to ``None``.
spoiler: :class:`bool`
Whether to flag this thumbnail as a spoiler. Defaults to ``False``.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
"""
__slots__ = (
'_media',
'description',
'spoiler',
)
__item_repr_attributes__ = (
'media',
'description',
'spoiler',
'row',
'id',
)
def __init__(
self,
media: Union[str, File, UnfurledMediaItem],
*,
description: Optional[str] = MISSING,
spoiler: bool = MISSING,
id: Optional[int] = None,
) -> None:
super().__init__()
if isinstance(media, File):
description = description if description is not MISSING else media.description
spoiler = spoiler if spoiler is not MISSING else media.spoiler
media = media.uri
self._media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media
self.description: Optional[str] = None if description is MISSING else description
self.spoiler: bool = bool(spoiler)
self.id = id
@property
def width(self):
return 5
@property
def media(self) -> UnfurledMediaItem:
""":class:`discord.UnfurledMediaItem`: This thumbnail unfurled media data."""
return self._media
@media.setter
def media(self, value: Union[str, File, UnfurledMediaItem]) -> None:
if isinstance(value, str):
self._media = UnfurledMediaItem(value)
elif isinstance(value, UnfurledMediaItem):
self._media = value
elif isinstance(value, File):
self._media = UnfurledMediaItem(value.uri)
else:
raise TypeError(f'expected a str or UnfurledMediaItem, not {value.__class__.__name__!r}')
@property
def type(self) -> Literal[ComponentType.thumbnail]:
return ComponentType.thumbnail
def _is_v2(self) -> bool:
return True
def to_component_dict(self) -> Dict[str, Any]:
base = {
'type': self.type.value,
'spoiler': self.spoiler,
'media': self.media.to_dict(),
'description': self.description,
}
if self.id is not None:
base['id'] = self.id
return base
@classmethod
def from_component(cls, component: ThumbnailComponent) -> Self:
return cls(
media=component.media.url,
description=component.description,
spoiler=component.spoiler,
id=component.id,
)

File diff suppressed because it is too large Load diff