On branch DiscordProfile
Initial commit
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
discord.ui
|
||||
~~~~~~~~~~
|
||||
|
||||
UI Kit helper for the Discord API
|
||||
|
||||
:copyright: (c) 2015-2021 Rapptz & (c) 2021-present Pycord Development
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from .action_row import *
|
||||
from .button import *
|
||||
from .checkbox import *
|
||||
from .checkbox_group import *
|
||||
from .container import *
|
||||
from .core import *
|
||||
from .file import *
|
||||
from .file_upload import *
|
||||
from .input_text import *
|
||||
from .item import *
|
||||
from .label import *
|
||||
from .media_gallery import *
|
||||
from .modal import *
|
||||
from .radio_group import *
|
||||
from .section import *
|
||||
from .select import *
|
||||
from .separator import *
|
||||
from .text_display import *
|
||||
from .thumbnail import *
|
||||
from .view import *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,436 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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 collections.abc import Sequence
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, ClassVar, Iterator, Literal, TypeVar, overload
|
||||
|
||||
from ..components import ActionRow as ActionRowComponent
|
||||
from ..components import SelectDefaultValue, SelectOption, _component_factory
|
||||
from ..enums import ButtonStyle, ChannelType, ComponentType
|
||||
from ..utils import find, get
|
||||
from .button import Button
|
||||
from .file import File
|
||||
from .item import ItemCallbackType, ViewItem
|
||||
from .select import Select
|
||||
|
||||
__all__ = ("ActionRow",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..emoji import AppEmoji, GuildEmoji
|
||||
from ..partial_emoji import PartialEmoji, _EmojiTag
|
||||
from ..types.components import ActionRow as ActionRowPayload
|
||||
from .view import DesignerView
|
||||
|
||||
|
||||
A = TypeVar("A", bound="ActionRow")
|
||||
V = TypeVar("V", bound="DesignerView", covariant=True)
|
||||
|
||||
|
||||
class ActionRow(ViewItem[V]):
|
||||
"""Represents a UI Action Row used in :class:`discord.ui.DesignerView`.
|
||||
|
||||
The items supported are as follows:
|
||||
|
||||
- :class:`discord.ui.Select`
|
||||
- :class:`discord.ui.Button`
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*items: :class:`ViewItem`
|
||||
The initial items in this action row.
|
||||
id: Optional[:class:`int`]
|
||||
The action's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"children",
|
||||
"id",
|
||||
)
|
||||
|
||||
__row_children_items__: ClassVar[list[ItemCallbackType]] = []
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
children: list[ItemCallbackType] = []
|
||||
for base in reversed(cls.__mro__):
|
||||
for member in base.__dict__.values():
|
||||
if hasattr(member, "__discord_ui_model_type__"):
|
||||
children.append(member)
|
||||
|
||||
cls.__row_children_items__ = children
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*items: ViewItem,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.children: list[ViewItem] = []
|
||||
|
||||
self._underlying = self._generate_underlying(id=id)
|
||||
|
||||
for func in self.__row_children_items__:
|
||||
item: ViewItem = func.__discord_ui_model_type__(
|
||||
**func.__discord_ui_model_kwargs__
|
||||
)
|
||||
item.callback = partial(func, self, item)
|
||||
self.add_item(item)
|
||||
setattr(self, func.__name__, item)
|
||||
for i in items:
|
||||
self.add_item(i)
|
||||
|
||||
@property
|
||||
def items(self) -> list[ViewItem]:
|
||||
return self.children
|
||||
|
||||
@items.setter
|
||||
def items(self, value: list[ViewItem]) -> None:
|
||||
self.children = value
|
||||
|
||||
def _add_component_from_item(self, item: ViewItem):
|
||||
self.underlying.children.append(item._generate_underlying())
|
||||
|
||||
def _set_components(self, items: list[ViewItem]):
|
||||
self.underlying.children.clear()
|
||||
for item in items:
|
||||
self._add_component_from_item(item)
|
||||
|
||||
def _generate_underlying(self, id: int | None = None) -> ActionRowComponent:
|
||||
super()._generate_underlying(ActionRowComponent)
|
||||
row = ActionRowComponent._raw_construct(
|
||||
type=ComponentType.action_row,
|
||||
id=id or self.id,
|
||||
children=[],
|
||||
)
|
||||
for i in self.children:
|
||||
row.children.append(i._generate_underlying())
|
||||
return row
|
||||
|
||||
def add_item(self, item: ViewItem) -> Self:
|
||||
"""Adds an item to the action row.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`ViewItem`
|
||||
The item to add to the action row.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
A :class:`ViewItem` was not passed.
|
||||
"""
|
||||
|
||||
if not isinstance(item, (Select, Button)):
|
||||
raise TypeError(f"expected Select or Button, not {item.__class__!r}")
|
||||
if item.row:
|
||||
raise ValueError(f"{item.__class__!r}.row is not supported in ActionRow")
|
||||
if self.width + item.width > 5:
|
||||
raise ValueError(f"Not enough space left on this ActionRow")
|
||||
|
||||
item.parent = self
|
||||
|
||||
self.children.append(item)
|
||||
self._add_component_from_item(item)
|
||||
return self
|
||||
|
||||
def remove_item(self, item: ViewItem | str | int) -> Self:
|
||||
"""Removes an item from the action row. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: Union[:class:`ViewItem`, :class:`int`, :class:`str`]
|
||||
The item, ``id``, or item ``custom_id`` to remove from the action row.
|
||||
"""
|
||||
|
||||
if isinstance(item, (str, int)):
|
||||
item = self.get_item(item)
|
||||
try:
|
||||
self.children.remove(item)
|
||||
item.parent = None
|
||||
except ValueError:
|
||||
pass
|
||||
return self
|
||||
|
||||
def get_item(self, id: str | int) -> ViewItem | None:
|
||||
"""Get an item from this action row. Roughly equivalent to `utils.get(row.children, ...)`.
|
||||
If an ``int`` is provided, the item will be retrieved by ``id``, otherwise by ``custom_id``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id: Union[:class:`str`, :class:`int`]
|
||||
The id or custom_id of the item to get.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`ViewItem`]
|
||||
The item with the matching ``id`` or ``custom_id`` if it exists.
|
||||
"""
|
||||
if not id:
|
||||
return None
|
||||
attr = "id" if isinstance(id, int) else "custom_id"
|
||||
child = find(lambda i: getattr(i, attr, None) == id, self.children)
|
||||
return child
|
||||
|
||||
def add_button(
|
||||
self,
|
||||
*,
|
||||
style: ButtonStyle = ButtonStyle.secondary,
|
||||
label: str | None = None,
|
||||
disabled: bool = False,
|
||||
custom_id: str | None = None,
|
||||
url: str | None = None,
|
||||
emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None,
|
||||
sku_id: int | None = None,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Adds a :class:`Button` to the action row.
|
||||
|
||||
To append a pre-existing :class:`Button`, use the
|
||||
:meth:`add_item` method instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
style: :class:`discord.ButtonStyle`
|
||||
The style of the button.
|
||||
custom_id: Optional[:class:`str`]
|
||||
The custom ID of the button that gets received during an interaction.
|
||||
If this button is for a URL, it does not have a custom ID.
|
||||
url: Optional[:class:`str`]
|
||||
The URL this button sends you to.
|
||||
disabled: :class:`bool`
|
||||
Whether the button is disabled or not.
|
||||
label: Optional[:class:`str`]
|
||||
The label of the button, if any. Maximum of 80 chars.
|
||||
emoji: Optional[Union[:class:`.PartialEmoji`, :class:`GuildEmoji`, :class:`AppEmoji`, :class:`str`]]
|
||||
The emoji of the button, if any.
|
||||
sku_id: Optional[Union[:class:`int`]]
|
||||
The ID of the SKU this button refers to.
|
||||
id: Optional[:class:`int`]
|
||||
The button's ID.
|
||||
"""
|
||||
|
||||
button = Button(
|
||||
style=style,
|
||||
label=label,
|
||||
disabled=disabled,
|
||||
custom_id=custom_id,
|
||||
url=url,
|
||||
emoji=emoji,
|
||||
sku_id=sku_id,
|
||||
id=id,
|
||||
)
|
||||
|
||||
return self.add_item(button)
|
||||
|
||||
@overload
|
||||
def add_select(
|
||||
self,
|
||||
select_type: Literal[ComponentType.string_select] = ...,
|
||||
*,
|
||||
custom_id: str | None = ...,
|
||||
placeholder: str | None = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
options: list[SelectOption] | None = ...,
|
||||
disabled: bool = ...,
|
||||
id: int | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def add_select(
|
||||
self,
|
||||
select_type: Literal[ComponentType.channel_select] = ...,
|
||||
*,
|
||||
custom_id: str | None = ...,
|
||||
placeholder: str | None = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
channel_types: list[ChannelType] | None = ...,
|
||||
disabled: bool = ...,
|
||||
id: int | None = ...,
|
||||
default_values: Sequence[SelectDefaultValue] | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def add_select(
|
||||
self,
|
||||
select_type: Literal[
|
||||
ComponentType.user_select,
|
||||
ComponentType.role_select,
|
||||
ComponentType.mentionable_select,
|
||||
] = ...,
|
||||
*,
|
||||
custom_id: str | None = ...,
|
||||
placeholder: str | None = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
disabled: bool = ...,
|
||||
id: int | None = ...,
|
||||
default_values: Sequence[SelectDefaultValue] | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
def add_select(
|
||||
self,
|
||||
select_type: ComponentType = ComponentType.string_select,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
placeholder: str | None = None,
|
||||
min_values: int = 1,
|
||||
max_values: int = 1,
|
||||
options: list[SelectOption] | None = None,
|
||||
channel_types: list[ChannelType] | None = None,
|
||||
disabled: bool = False,
|
||||
id: int | None = None,
|
||||
default_values: Sequence[SelectDefaultValue] | None = None,
|
||||
) -> Self:
|
||||
"""Adds a :class:`Select` to the action row.
|
||||
|
||||
To append a pre-existing :class:`Select`, use the
|
||||
:meth:`add_item` method instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
select_type: :class:`discord.ComponentType`
|
||||
The type of select to create. Must be one of
|
||||
:attr:`discord.ComponentType.string_select`, :attr:`discord.ComponentType.user_select`,
|
||||
:attr:`discord.ComponentType.role_select`, :attr:`discord.ComponentType.mentionable_select`,
|
||||
or :attr:`discord.ComponentType.channel_select`.
|
||||
custom_id: :class:`str`
|
||||
The custom ID of the select menu that gets received during an interaction.
|
||||
If not given then one is generated for you.
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text that is shown if nothing is selected, if any.
|
||||
min_values: :class:`int`
|
||||
The minimum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
max_values: :class:`int`
|
||||
The maximum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
options: List[:class:`discord.SelectOption`]
|
||||
A list of options that can be selected in this menu.
|
||||
Only valid for selects of type :attr:`discord.ComponentType.string_select`.
|
||||
channel_types: List[:class:`discord.ChannelType`]
|
||||
A list of channel types that can be selected in this menu.
|
||||
Only valid for selects of type :attr:`discord.ComponentType.channel_select`.
|
||||
disabled: :class:`bool`
|
||||
Whether the select is disabled or not. Defaults to ``False``.
|
||||
id: Optional[:class:`int`]
|
||||
The select menu's ID.
|
||||
default_values: Optional[Sequence[Union[:class:`discord.SelectDefaultValue`, :class:`discord.abc.Snowflake`]]]
|
||||
The default values of this select. Only applicable if :attr:`.select_type` is not :attr:`discord.ComponentType.string_select`.
|
||||
|
||||
These can be either :class:`discord.SelectDefaultValue` instances or models, which will be converted into :class:`discord.SelectDefaultValue`
|
||||
instances.
|
||||
"""
|
||||
|
||||
select = Select(
|
||||
select_type=select_type,
|
||||
custom_id=custom_id,
|
||||
placeholder=placeholder,
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
options=options or [],
|
||||
channel_types=channel_types or [],
|
||||
disabled=disabled,
|
||||
id=id,
|
||||
default_values=default_values,
|
||||
)
|
||||
|
||||
return self.add_item(select)
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return any(item.is_dispatchable() for item in self.children)
|
||||
|
||||
def is_persistent(self) -> bool:
|
||||
return all(item.is_persistent() for item in self.children)
|
||||
|
||||
def refresh_component(self, component: ActionRowComponent) -> None:
|
||||
self.underlying = component
|
||||
for i, y in enumerate(component.components):
|
||||
x = self.children[i]
|
||||
x.refresh_component(y)
|
||||
|
||||
def disable_all_items(self, *, exclusions: list[ViewItem] | None = None) -> Self:
|
||||
"""
|
||||
Disables all items in the row.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exclusions: Optional[List[:class:`ViewItem`]]
|
||||
A list of items in `self.children` to not disable.
|
||||
"""
|
||||
for item in self.walk_items():
|
||||
if exclusions is None or item not in exclusions:
|
||||
item.disabled = True
|
||||
return self
|
||||
|
||||
def enable_all_items(self, *, exclusions: list[ViewItem] | None = None) -> Self:
|
||||
"""
|
||||
Enables all items in the row.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exclusions: Optional[List[:class:`ViewItem`]]
|
||||
A list of items in `self.children` to not enable.
|
||||
"""
|
||||
for item in self.walk_items():
|
||||
if exclusions is None or item not in exclusions:
|
||||
item.disabled = False
|
||||
return self
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""Return the sum of the items' widths."""
|
||||
t = 0
|
||||
for item in self.children:
|
||||
t += 1 if item.underlying.type is ComponentType.button else 5
|
||||
return t
|
||||
|
||||
def walk_items(self) -> Iterator[ViewItem]:
|
||||
yield from self.children
|
||||
|
||||
def to_component_dict(self) -> ActionRowPayload:
|
||||
self._underlying = self._generate_underlying()
|
||||
return super().to_component_dict()
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[A], component: ActionRowComponent) -> A:
|
||||
from .view import _component_to_item, _walk_all_components
|
||||
|
||||
items = [
|
||||
_component_to_item(c) for c in _walk_all_components(component.components)
|
||||
]
|
||||
return cls(
|
||||
*items,
|
||||
id=component.id,
|
||||
)
|
||||
|
||||
callback = None
|
||||
@@ -0,0 +1,419 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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 inspect
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Callable, TypeVar
|
||||
|
||||
from ..components import Button as ButtonComponent
|
||||
from ..enums import ButtonStyle, ComponentType
|
||||
from ..partial_emoji import PartialEmoji, _EmojiTag
|
||||
from .item import ItemCallbackType, ViewItem
|
||||
|
||||
__all__ = (
|
||||
"Button",
|
||||
"button",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..emoji import AppEmoji, GuildEmoji
|
||||
from ..types.components import ButtonComponent as ButtonComponentPayload
|
||||
from .view import BaseView
|
||||
|
||||
B = TypeVar("B", bound="Button")
|
||||
V = TypeVar("V", bound="BaseView", covariant=True)
|
||||
|
||||
|
||||
class Button(ViewItem[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.
|
||||
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. Maximum of 80 chars.
|
||||
emoji: Optional[Union[:class:`.PartialEmoji`, :class:`GuildEmoji`, :class:`AppEmoji`, :class:`str`]]
|
||||
The emoji of the button, if available.
|
||||
sku_id: Optional[Union[:class:`int`]]
|
||||
The ID of the SKU this button refers to.
|
||||
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).
|
||||
|
||||
.. warning::
|
||||
|
||||
This parameter does not work in :class:`ActionRow`.
|
||||
|
||||
id: Optional[:class:`int`]
|
||||
The button's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"style",
|
||||
"url",
|
||||
"disabled",
|
||||
"label",
|
||||
"emoji",
|
||||
"sku_id",
|
||||
"row",
|
||||
"custom_id",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
style: ButtonStyle = ButtonStyle.secondary,
|
||||
label: str | None = None,
|
||||
disabled: bool = False,
|
||||
custom_id: str | None = None,
|
||||
url: str | None = None,
|
||||
emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None,
|
||||
sku_id: int | None = None,
|
||||
row: int | None = None,
|
||||
id: int | None = None,
|
||||
):
|
||||
self._row: int | None = None
|
||||
self._rendered_row: int | None = None
|
||||
super().__init__()
|
||||
if label and len(str(label)) > 80:
|
||||
raise ValueError("label must be 80 characters or fewer")
|
||||
if custom_id is not None and len(str(custom_id)) > 100:
|
||||
raise ValueError("custom_id must be 100 characters or fewer")
|
||||
if custom_id is not None and url is not None:
|
||||
raise TypeError("cannot mix both url and custom_id with Button")
|
||||
if sku_id is not None and url is not None:
|
||||
raise TypeError("cannot mix both url and sku_id with Button")
|
||||
if custom_id is not None and sku_id is not None:
|
||||
raise TypeError("cannot mix both sku_id and custom_id with Button")
|
||||
|
||||
if not isinstance(custom_id, str) and custom_id is not None:
|
||||
raise TypeError(
|
||||
f"expected custom_id to be str, not {custom_id.__class__.__name__}"
|
||||
)
|
||||
|
||||
self._provided_custom_id = custom_id is not None
|
||||
if url is None and custom_id is None and sku_id is None:
|
||||
custom_id = os.urandom(16).hex()
|
||||
|
||||
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(
|
||||
"expected emoji to be str, GuildEmoji, AppEmoji, or PartialEmoji not"
|
||||
f" {emoji.__class__}"
|
||||
)
|
||||
|
||||
self.row = row
|
||||
self._underlying = self._generate_underlying(
|
||||
custom_id=custom_id,
|
||||
url=url,
|
||||
disabled=disabled,
|
||||
label=label,
|
||||
style=style,
|
||||
emoji=emoji,
|
||||
sku_id=sku_id,
|
||||
id=id,
|
||||
)
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
style: ButtonStyle | None = None,
|
||||
label: str | None = None,
|
||||
disabled: bool | None = None,
|
||||
custom_id: str | None = None,
|
||||
url: str | None = None,
|
||||
emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None,
|
||||
sku_id: int | None = None,
|
||||
id: int | None = None,
|
||||
) -> ButtonComponent:
|
||||
super()._generate_underlying(ButtonComponent)
|
||||
return ButtonComponent._raw_construct(
|
||||
type=ComponentType.button,
|
||||
custom_id=custom_id or self.custom_id,
|
||||
url=url or self.url,
|
||||
disabled=disabled if disabled is not None else self.disabled,
|
||||
label=label or self.label,
|
||||
style=style or self.style,
|
||||
emoji=emoji or self.emoji,
|
||||
sku_id=sku_id or self.sku_id,
|
||||
id=id or self.id,
|
||||
)
|
||||
|
||||
@property
|
||||
def style(self) -> ButtonStyle:
|
||||
"""The style of the button."""
|
||||
return self.underlying.style
|
||||
|
||||
@style.setter
|
||||
def style(self, value: ButtonStyle):
|
||||
self.underlying.style = value
|
||||
|
||||
@property
|
||||
def custom_id(self) -> str | None:
|
||||
"""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: str | None):
|
||||
if value is not None and not isinstance(value, str):
|
||||
raise TypeError("custom_id must be None or str")
|
||||
if value and len(value) > 100:
|
||||
raise ValueError("custom_id must be 100 characters or fewer")
|
||||
self.underlying.custom_id = value
|
||||
self._provided_custom_id = value is not None
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
"""The URL this button sends you to."""
|
||||
return self.underlying.url
|
||||
|
||||
@url.setter
|
||||
def url(self, value: 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:
|
||||
"""Whether the button is disabled or not."""
|
||||
return self.underlying.disabled
|
||||
|
||||
@disabled.setter
|
||||
def disabled(self, value: bool):
|
||||
self.underlying.disabled = bool(value)
|
||||
|
||||
@property
|
||||
def label(self) -> str | None:
|
||||
"""The label of the button, if available."""
|
||||
return self.underlying.label
|
||||
|
||||
@label.setter
|
||||
def label(self, value: str | None):
|
||||
if value and len(str(value)) > 80:
|
||||
raise ValueError("label must be 80 characters or fewer")
|
||||
self.underlying.label = str(value) if value is not None else value
|
||||
|
||||
@property
|
||||
def emoji(self) -> PartialEmoji | None:
|
||||
"""The emoji of the button, if available."""
|
||||
return self.underlying.emoji
|
||||
|
||||
@emoji.setter
|
||||
def emoji(self, value: str | GuildEmoji | AppEmoji | PartialEmoji | None): # type: ignore
|
||||
if value is None:
|
||||
self.underlying.emoji = None
|
||||
elif isinstance(value, str):
|
||||
self.underlying.emoji = PartialEmoji.from_str(value)
|
||||
elif isinstance(value, _EmojiTag):
|
||||
self.underlying.emoji = value._to_partial()
|
||||
else:
|
||||
raise TypeError(
|
||||
"expected str, GuildEmoji, AppEmoji, or PartialEmoji, received"
|
||||
f" {value.__class__} instead"
|
||||
)
|
||||
|
||||
@property
|
||||
def sku_id(self) -> int | None:
|
||||
"""The ID of the SKU this button refers to."""
|
||||
return self.underlying.sku_id
|
||||
|
||||
@sku_id.setter
|
||||
def sku_id(self, value: int | None): # type: ignore
|
||||
if value is None:
|
||||
self.underlying.sku_id = None
|
||||
elif isinstance(value, int):
|
||||
self.underlying.sku_id = value
|
||||
else:
|
||||
raise TypeError(f"expected int or None, received {value.__class__} instead")
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
"""Gets the width of the item in the UI layout.
|
||||
|
||||
The width determines how much horizontal space this item occupies within its row.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`int`
|
||||
The width of the item. Buttons have a width of 1.
|
||||
"""
|
||||
return 1
|
||||
|
||||
@property
|
||||
def row(self) -> int | None:
|
||||
"""Gets or sets the row position of this item within its parent view.
|
||||
|
||||
The row position determines the vertical placement of the item in the UI.
|
||||
The value must be an integer between 0 and 4 (inclusive), or ``None`` to indicate
|
||||
that no specific row is set.
|
||||
This attribute is not compatible with :class:`discord.ui.DesignerView`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`int`]
|
||||
The row position of the item, or ``None`` if not explicitly set.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the row value is not ``None`` and is outside the range [0, 4].
|
||||
"""
|
||||
return self._row
|
||||
|
||||
@row.setter
|
||||
def row(self, value: int | None):
|
||||
if value is None or 5 > value >= 0:
|
||||
self._row = value
|
||||
else:
|
||||
raise ValueError("row cannot be negative or greater than or equal to 5")
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[B], button: ButtonComponent) -> B:
|
||||
return cls(
|
||||
style=button.style,
|
||||
label=button.label,
|
||||
disabled=button.disabled,
|
||||
custom_id=button.custom_id,
|
||||
url=button.url,
|
||||
emoji=button.emoji,
|
||||
sku_id=button.sku_id,
|
||||
row=None,
|
||||
id=button.id,
|
||||
)
|
||||
|
||||
def to_component_dict(self) -> ButtonComponentPayload:
|
||||
return super().to_component_dict()
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return (self.custom_id is not None) and (
|
||||
bool(self.view._store) if self.view else True
|
||||
)
|
||||
|
||||
def is_storable(self) -> bool:
|
||||
return self.is_dispatchable()
|
||||
|
||||
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 button(
|
||||
*,
|
||||
label: str | None = None,
|
||||
custom_id: str | None = None,
|
||||
disabled: bool = False,
|
||||
style: ButtonStyle = ButtonStyle.secondary,
|
||||
emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None,
|
||||
row: int | None = None,
|
||||
id: int | None = None,
|
||||
) -> Callable[[ItemCallbackType[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`, :class:`discord.ui.ActionRow` or :class:`discord.ui.Section`, the :class:`discord.ui.Button` being pressed, and
|
||||
the :class:`discord.Interaction` you receive.
|
||||
|
||||
.. note::
|
||||
|
||||
Premium and link buttons cannot be created with this decorator. Consider
|
||||
creating a :class:`Button` object manually instead. These types of
|
||||
buttons do not have a callback associated since Discord doesn't handle
|
||||
them when clicked.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label: Optional[:class:`str`]
|
||||
The label of the button, if any.
|
||||
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.
|
||||
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:`GuildEmoji`, :class:`AppEmoji`, :class:`.PartialEmoji`]]
|
||||
The emoji of the button. This can be in string form or a :class:`.PartialEmoji`
|
||||
or a full :class:`GuildEmoji` or :class:`AppEmoji`.
|
||||
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).
|
||||
|
||||
.. warning::
|
||||
|
||||
This parameter does not work in :class:`ActionRow`.
|
||||
"""
|
||||
|
||||
def decorator(func: ItemCallbackType) -> ItemCallbackType:
|
||||
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,
|
||||
"id": id,
|
||||
}
|
||||
return func
|
||||
|
||||
return decorator # type: ignore # lie to the type checkers, because after a View is instated, the button callback is converted into a Button instance
|
||||
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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
|
||||
|
||||
from ..components import Checkbox as CheckboxComponent
|
||||
from ..enums import ComponentType
|
||||
from .item import ModalItem
|
||||
|
||||
__all__ = ("Checkbox",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from ..types.components import CheckboxComponent as CheckboxComponentPayload
|
||||
|
||||
|
||||
class Checkbox(ModalItem):
|
||||
"""Represents a UI Checkbox component.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the checkbox that gets received during an interaction.
|
||||
default: Optional[:class:`bool`]
|
||||
Whether this checkbox is selected by default or not.
|
||||
id: Optional[:class:`int`]
|
||||
The checkbox's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"default",
|
||||
"custom_id",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
default: bool = False,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
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 not isinstance(default, bool):
|
||||
raise TypeError(f"default must be bool, not {default.__class__.__name__}")
|
||||
custom_id = os.urandom(16).hex() if custom_id is None else custom_id
|
||||
self._value: bool | None = None
|
||||
|
||||
self._underlying: CheckboxComponent = self._generate_underlying(
|
||||
custom_id=custom_id,
|
||||
default=default,
|
||||
id=id,
|
||||
)
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
custom_id: str | None = None,
|
||||
default: bool | None = None,
|
||||
id: int | None = None,
|
||||
) -> CheckboxComponent:
|
||||
super()._generate_underlying(CheckboxComponent)
|
||||
return CheckboxComponent._raw_construct(
|
||||
type=ComponentType.checkbox,
|
||||
custom_id=custom_id or self.custom_id,
|
||||
default=default if default is not None else self.default,
|
||||
id=id or self.id,
|
||||
)
|
||||
|
||||
@property
|
||||
def custom_id(self) -> str:
|
||||
"""The custom id that gets received during an interaction."""
|
||||
return self.underlying.custom_id
|
||||
|
||||
@custom_id.setter
|
||||
def custom_id(self, value: str):
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"custom_id must be str not {value.__class__.__name__}")
|
||||
if len(value) > 100:
|
||||
raise ValueError("custom_id must be 100 characters or fewer")
|
||||
self.underlying.custom_id = value
|
||||
|
||||
@property
|
||||
def default(self) -> bool:
|
||||
"""Whether this checkbox is selected by default or not. Defaults to ``False``"""
|
||||
return self.underlying.default
|
||||
|
||||
@default.setter
|
||||
def default(self, value: bool):
|
||||
if not isinstance(value, bool):
|
||||
raise TypeError(f"default must be bool, not {value.__class__.__name__}")
|
||||
self.underlying.default = bool(value)
|
||||
|
||||
@property
|
||||
def value(self) -> bool | None:
|
||||
"""Whether this checkbox was selected or not by the user. This will be ``None`` if the checkbox has not been submitted via a modal yet."""
|
||||
return self._value
|
||||
|
||||
def to_component_dict(self) -> CheckboxComponentPayload:
|
||||
return self.underlying.to_dict()
|
||||
|
||||
def refresh_state(self, data) -> None:
|
||||
self._value = data.get("value", None)
|
||||
|
||||
def refresh_from_modal(
|
||||
self, interaction: Interaction, data: CheckboxComponentPayload
|
||||
) -> None:
|
||||
return self.refresh_state(data)
|
||||
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..components import CheckboxGroup as CheckboxGroupComponent
|
||||
from ..components import CheckboxGroupOption
|
||||
from ..enums import ComponentType
|
||||
from ..utils import MISSING
|
||||
from .item import ModalItem
|
||||
|
||||
__all__ = ("CheckboxGroup",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from ..types.components import (
|
||||
CheckboxGroupComponent as CheckboxGroupComponentPayload,
|
||||
)
|
||||
|
||||
|
||||
class CheckboxGroup(ModalItem):
|
||||
"""Represents a UI Checkbox Group component.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the checkbox group that gets received during an interaction.
|
||||
options: List[:class:`discord.CheckboxGroupOption`]
|
||||
A list of options that can be selected in this group.
|
||||
min_values: Optional[:class:`int`]
|
||||
The minimum number of options that must be selected.
|
||||
Defaults to 0 and must be between 0 and 10, inclusive.
|
||||
max_values: Optional[:class:`int`]
|
||||
The maximum number of options that can be selected.
|
||||
Must be between 1 and 10, inclusive.
|
||||
required: Optional[:class:`bool`]
|
||||
Whether an option selection is required or not. Defaults to ``True``.
|
||||
id: Optional[:class:`int`]
|
||||
The checkbox group's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"options",
|
||||
"required",
|
||||
"min_values",
|
||||
"max_values",
|
||||
"custom_id",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
options: list[CheckboxGroupOption] | None = None,
|
||||
min_values: int | None = None,
|
||||
max_values: int | None = None,
|
||||
required: bool = True,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
if min_values and (min_values < 0 or min_values > 10):
|
||||
raise ValueError("min_values must be between 0 and 10")
|
||||
if max_values and (max_values < 1 or max_values > 10):
|
||||
raise ValueError("max_values must be between 1 and 10")
|
||||
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 not isinstance(required, bool):
|
||||
raise TypeError(f"required must be bool not {required.__class__.__name__}")
|
||||
custom_id = os.urandom(16).hex() if custom_id is None else custom_id
|
||||
self._selected_values: list[str] | None = None
|
||||
|
||||
self._underlying: CheckboxGroupComponent = self._generate_underlying(
|
||||
custom_id=custom_id,
|
||||
options=[],
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
required=required,
|
||||
id=id,
|
||||
)
|
||||
if options:
|
||||
self.options = options
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
custom_id: str | None = None,
|
||||
options: list[CheckboxGroupOption] | None = None,
|
||||
min_values: int | None = None,
|
||||
max_values: int | None = None,
|
||||
required: bool | None = None,
|
||||
id: int | None = None,
|
||||
) -> CheckboxGroupComponent:
|
||||
super()._generate_underlying(CheckboxGroupComponent)
|
||||
return CheckboxGroupComponent._raw_construct(
|
||||
type=ComponentType.checkbox_group,
|
||||
custom_id=custom_id or self.custom_id,
|
||||
options=options if options is not None else self.options,
|
||||
min_values=min_values if min_values is not None else self.min_values,
|
||||
max_values=max_values if max_values is not None else self.max_values,
|
||||
required=required if required is not None else self.required,
|
||||
id=id or self.id,
|
||||
)
|
||||
|
||||
@property
|
||||
def custom_id(self) -> str:
|
||||
"""The custom id that gets received during an interaction."""
|
||||
return self.underlying.custom_id
|
||||
|
||||
@custom_id.setter
|
||||
def custom_id(self, value: str):
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"custom_id must be str not {value.__class__.__name__}")
|
||||
if len(value) > 100:
|
||||
raise ValueError("custom_id must be 100 characters or fewer")
|
||||
self.underlying.custom_id = value
|
||||
|
||||
@property
|
||||
def min_values(self) -> int | None:
|
||||
"""The minimum number of options that must be selected."""
|
||||
return self.underlying.min_values
|
||||
|
||||
@min_values.setter
|
||||
def min_values(self, value: int | None):
|
||||
if value and not isinstance(value, int):
|
||||
raise TypeError(
|
||||
f"min_values must be None or int not {value.__class__.__name__}"
|
||||
)
|
||||
if value and (value < 0 or value > 10):
|
||||
raise ValueError("min_values must be between 0 and 10")
|
||||
self.underlying.min_values = value
|
||||
|
||||
@property
|
||||
def max_values(self) -> int | None:
|
||||
"""The maximum number of options that can be selected."""
|
||||
return self.underlying.max_values
|
||||
|
||||
@max_values.setter
|
||||
def max_values(self, value: int | None):
|
||||
if value and not isinstance(value, int):
|
||||
raise TypeError(
|
||||
f"max_values must be None or int not {value.__class__.__name__}"
|
||||
)
|
||||
if value and (value < 1 or value > 10):
|
||||
raise ValueError("max_values must be between 1 and 10")
|
||||
self.underlying.max_values = value
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
"""Whether an option selection is required or not. Defaults to ``True``"""
|
||||
return self.underlying.required
|
||||
|
||||
@required.setter
|
||||
def required(self, value: bool):
|
||||
if not isinstance(value, bool):
|
||||
raise TypeError(f"required must be bool, not {value.__class__.__name__}")
|
||||
self.underlying.required = bool(value)
|
||||
|
||||
@property
|
||||
def values(self) -> list[str] | None:
|
||||
"""The values selected by the user. This will be ``None`` if the checkbox group has not been submitted in a modal yet."""
|
||||
return self._selected_values
|
||||
|
||||
@property
|
||||
def options(self) -> list[CheckboxGroupOption]:
|
||||
"""A list of options that can be selected in this group."""
|
||||
return self.underlying.options
|
||||
|
||||
@options.setter
|
||||
def options(self, value: list[CheckboxGroupOption]):
|
||||
if not isinstance(value, list):
|
||||
raise TypeError("options must be a list of CheckboxGroupOption")
|
||||
if len(value) > 10:
|
||||
raise ValueError("you may only provide up to 10 options.")
|
||||
if not all(isinstance(obj, CheckboxGroupOption) for obj in value):
|
||||
raise TypeError("all list items must subclass CheckboxGroupOption")
|
||||
|
||||
self.underlying.options = value
|
||||
|
||||
def add_option(
|
||||
self,
|
||||
*,
|
||||
label: str,
|
||||
value: str = MISSING,
|
||||
description: str | None = None,
|
||||
default: bool = False,
|
||||
) -> Self:
|
||||
"""Adds an option to the checkbox group.
|
||||
|
||||
To append a pre-existing :class:`discord.CheckboxGroupOption` use the
|
||||
:meth:`append_option` method instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label: :class:`str`
|
||||
The label of the option. This is displayed to users.
|
||||
Can only be up to 100 characters.
|
||||
value: :class:`str`
|
||||
The value of the option. This is not displayed to users.
|
||||
If not given, defaults to the label. Can only be up to 100 characters.
|
||||
description: Optional[:class:`str`]
|
||||
An additional description of the option, if any.
|
||||
Can only be up to 100 characters.
|
||||
default: :class:`bool`
|
||||
Whether this option is selected by default.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
The number of options exceeds 10.
|
||||
"""
|
||||
|
||||
option = CheckboxGroupOption(
|
||||
label=label,
|
||||
value=value,
|
||||
description=description,
|
||||
default=default,
|
||||
)
|
||||
|
||||
return self.append_option(option)
|
||||
|
||||
def append_option(self, option: CheckboxGroupOption) -> Self:
|
||||
"""Appends an option to the checkbox group.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
option: :class:`discord.CheckboxGroupOption`
|
||||
The option to append to the checkbox group.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
The number of options exceeds 10.
|
||||
"""
|
||||
|
||||
if len(self.underlying.options) >= 10:
|
||||
raise ValueError("maximum number of options already provided")
|
||||
|
||||
self.underlying.options.append(option)
|
||||
return self
|
||||
|
||||
def clear_options(self) -> Self:
|
||||
"""Remove all options from the checkbox group."""
|
||||
|
||||
self.underlying.options.clear()
|
||||
return self
|
||||
|
||||
def to_component_dict(self) -> CheckboxGroupComponentPayload:
|
||||
return self.underlying.to_dict()
|
||||
|
||||
def refresh_state(self, data) -> None:
|
||||
self._selected_values = data.get("values", [])
|
||||
|
||||
def refresh_from_modal(
|
||||
self, interaction: Interaction, data: CheckboxGroupComponentPayload
|
||||
) -> None:
|
||||
return self.refresh_state(data)
|
||||
@@ -0,0 +1,442 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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, Iterator, TypeVar
|
||||
|
||||
from ..colour import Colour
|
||||
from ..components import Container as ContainerComponent
|
||||
from ..components import MediaGalleryItem, _component_factory
|
||||
from ..enums import ComponentType, SeparatorSpacingSize
|
||||
from ..utils import find, get
|
||||
from .action_row import ActionRow
|
||||
from .button import Button
|
||||
from .file import File
|
||||
from .item import ItemCallbackType, ViewItem
|
||||
from .media_gallery import MediaGallery
|
||||
from .section import Section
|
||||
from .select import Select
|
||||
from .separator import Separator
|
||||
from .text_display import TextDisplay
|
||||
|
||||
__all__ = ("Container",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..types.components import ContainerComponent as ContainerComponentPayload
|
||||
from .view import DesignerView
|
||||
|
||||
|
||||
C = TypeVar("C", bound="Container")
|
||||
V = TypeVar("V", bound="DesignerView", covariant=True)
|
||||
|
||||
|
||||
class Container(ViewItem[V]):
|
||||
"""Represents a UI Container.
|
||||
|
||||
The current items supported are as follows:
|
||||
|
||||
- :class:`discord.ui.ActionRow`
|
||||
- :class:`discord.ui.Section`
|
||||
- :class:`discord.ui.TextDisplay`
|
||||
- :class:`discord.ui.MediaGallery`
|
||||
- :class:`discord.ui.File`
|
||||
- :class:`discord.ui.Separator`
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*items: :class:`ViewItem`
|
||||
The initial items in this container.
|
||||
colour: Union[:class:`Colour`, :class:`int`]
|
||||
The accent colour of the container. Aliased to ``color`` as well.
|
||||
spoiler: Optional[:class:`bool`]
|
||||
Whether this container has the spoiler overlay.
|
||||
id: Optional[:class:`int`]
|
||||
The container's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
items: List[:class:`ViewItem`]
|
||||
The list of items in this container.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"items",
|
||||
"colour",
|
||||
"spoiler",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
for base in reversed(cls.__mro__):
|
||||
for member in base.__dict__.values():
|
||||
if hasattr(member, "__discord_ui_model_type__"):
|
||||
raise ValueError(
|
||||
"The @button and @select decorators are incompatible with Container. Use ActionRow instead."
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*items: ViewItem,
|
||||
colour: int | Colour | None = None,
|
||||
color: int | Colour | None = None,
|
||||
spoiler: bool = False,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.items: list[ViewItem] = []
|
||||
|
||||
self._underlying = self._generate_underlying(
|
||||
id=id,
|
||||
accent_color=colour or color,
|
||||
spoiler=spoiler,
|
||||
)
|
||||
for i in items:
|
||||
self.add_item(i)
|
||||
|
||||
def _add_component_from_item(self, item: ViewItem):
|
||||
self.underlying.components.append(item._generate_underlying())
|
||||
|
||||
def _set_components(self, items: list[ViewItem]):
|
||||
self.underlying.components.clear()
|
||||
for item in items:
|
||||
self._add_component_from_item(item)
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
accent_color: int | Colour | None = None,
|
||||
spoiler: bool | None = None,
|
||||
id: int | None = None,
|
||||
) -> ContainerComponent:
|
||||
super()._generate_underlying(ContainerComponent)
|
||||
container = ContainerComponent._raw_construct(
|
||||
type=ComponentType.container,
|
||||
id=id or self.id,
|
||||
components=[],
|
||||
accent_color=Colour.resolve_value(accent_color or self.colour),
|
||||
spoiler=spoiler if spoiler is not None else self.spoiler,
|
||||
)
|
||||
for i in self.items:
|
||||
container.components.append(i._generate_underlying())
|
||||
return container
|
||||
|
||||
def add_item(self, item: ViewItem) -> Self:
|
||||
"""Adds an item to the container.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`ViewItem`
|
||||
The item to add to the container.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
A :class:`ViewItem` was not passed.
|
||||
"""
|
||||
|
||||
if not isinstance(item, ViewItem):
|
||||
raise TypeError(f"expected ViewItem not {item.__class__!r}")
|
||||
|
||||
if isinstance(item, (Button, Select)):
|
||||
raise TypeError(
|
||||
f"{item.__class__!r} cannot be added directly. Use ActionRow instead."
|
||||
)
|
||||
|
||||
item.parent = self
|
||||
|
||||
self.items.append(item)
|
||||
self._add_component_from_item(item)
|
||||
return self
|
||||
|
||||
def remove_item(self, item: ViewItem | str | int) -> Self:
|
||||
"""Removes an item from the container. If an int or str is passed, it will remove by Item :attr:`id` or ``custom_id`` respectively.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: Union[:class:`ViewItem`, :class:`int`, :class:`str`]
|
||||
The item, ``id``, or item ``custom_id`` to remove from the container.
|
||||
"""
|
||||
|
||||
if isinstance(item, (str, int)):
|
||||
item = self.get_item(item)
|
||||
try:
|
||||
if item.parent is self:
|
||||
self.items.remove(item)
|
||||
item.parent = None
|
||||
else:
|
||||
item.parent.remove_item(item)
|
||||
except ValueError:
|
||||
pass
|
||||
return self
|
||||
|
||||
def get_item(self, id: str | int) -> ViewItem | None:
|
||||
"""Get an item from this container. Roughly equivalent to `utils.get(container.items, ...)`.
|
||||
If an ``int`` is provided, the item will be retrieved by ``id``, otherwise by ``custom_id``.
|
||||
This method will also search for nested items.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id: Union[:class:`str`, :class:`int`]
|
||||
The id or custom_id of the item to get.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`ViewItem`]
|
||||
The item with the matching ``id`` or ``custom_id`` if it exists.
|
||||
"""
|
||||
if not id:
|
||||
return None
|
||||
attr = "id" if isinstance(id, int) else "custom_id"
|
||||
child = find(lambda i: getattr(i, attr, None) == id, self.items)
|
||||
if not child:
|
||||
for i in self.items:
|
||||
if hasattr(i, "get_item"):
|
||||
if child := i.get_item(id):
|
||||
return child
|
||||
return child
|
||||
|
||||
def add_row(
|
||||
self,
|
||||
*items: ViewItem,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Adds an :class:`ActionRow` to the container.
|
||||
|
||||
To append a pre-existing :class:`ActionRow`, use :meth:`add_item` instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*items: Union[:class:`Button`, :class:`Select`]
|
||||
The items this action row contains.
|
||||
id: Optiona[:class:`int`]
|
||||
The action row's ID.
|
||||
"""
|
||||
|
||||
row = ActionRow(*items, id=id)
|
||||
|
||||
return self.add_item(row)
|
||||
|
||||
def add_section(
|
||||
self,
|
||||
*items: ViewItem,
|
||||
accessory: ViewItem,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Adds a :class:`Section` to the container.
|
||||
|
||||
To append a pre-existing :class:`Section`, use the
|
||||
:meth:`add_item` method, instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*items: :class:`ViewItem`
|
||||
The items contained in this section, up to 3.
|
||||
Currently only supports :class:`~discord.ui.TextDisplay`.
|
||||
accessory: Optional[:class:`ViewItem`]
|
||||
The section's accessory. This is displayed in the top right of the section.
|
||||
Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`.
|
||||
id: Optional[:class:`int`]
|
||||
The section's ID.
|
||||
"""
|
||||
|
||||
section = Section(*items, accessory=accessory, id=id)
|
||||
|
||||
return self.add_item(section)
|
||||
|
||||
def add_text(self, content: str, id: int | None = None) -> Self:
|
||||
"""Adds a :class:`TextDisplay` to the container.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
content: :class:`str`
|
||||
The content of the TextDisplay
|
||||
id: Optiona[:class:`int`]
|
||||
The text displays' ID.
|
||||
"""
|
||||
|
||||
text = TextDisplay(content, id=id)
|
||||
|
||||
return self.add_item(text)
|
||||
|
||||
def add_gallery(
|
||||
self,
|
||||
*items: MediaGalleryItem,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Adds a :class:`MediaGallery` to the container.
|
||||
|
||||
To append a pre-existing :class:`MediaGallery`, use :meth:`add_item` instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*items: :class:`MediaGalleryItem`
|
||||
The media this gallery contains.
|
||||
id: Optiona[:class:`int`]
|
||||
The gallery's ID.
|
||||
"""
|
||||
|
||||
g = MediaGallery(*items, id=id)
|
||||
|
||||
return self.add_item(g)
|
||||
|
||||
def add_file(self, url: str, spoiler: bool = False, id: int | None = None) -> Self:
|
||||
"""Adds a :class:`File` to the container.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url: :class:`str`
|
||||
The URL of this file's media. This must be an ``attachment://`` URL that references a :class:`~discord.File`.
|
||||
spoiler: Optional[:class:`bool`]
|
||||
Whether the file has the spoiler overlay. Defaults to ``False``.
|
||||
id: Optiona[:class:`int`]
|
||||
The file's ID.
|
||||
"""
|
||||
|
||||
f = File(url, spoiler=spoiler, id=id)
|
||||
|
||||
return self.add_item(f)
|
||||
|
||||
def add_separator(
|
||||
self,
|
||||
*,
|
||||
divider: bool = True,
|
||||
spacing: SeparatorSpacingSize = SeparatorSpacingSize.small,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Adds a :class:`Separator` to the container.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
divider: :class:`bool`
|
||||
Whether the separator is a divider. Defaults to ``True``.
|
||||
spacing: :class:`~discord.SeparatorSpacingSize`
|
||||
The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`.
|
||||
id: Optional[:class:`int`]
|
||||
The separator's ID.
|
||||
"""
|
||||
|
||||
s = Separator(divider=divider, spacing=spacing, id=id)
|
||||
|
||||
return self.add_item(s)
|
||||
|
||||
def copy_text(self) -> str:
|
||||
"""Returns the text of all :class:`~discord.ui.TextDisplay` items in this container.
|
||||
Equivalent to the `Copy Text` option on Discord clients.
|
||||
"""
|
||||
return "\n".join(t for i in self.items if (t := i.copy_text()))
|
||||
|
||||
@property
|
||||
def spoiler(self) -> bool:
|
||||
"""Whether the container has the spoiler overlay. Defaults to ``False``."""
|
||||
return self.underlying.spoiler
|
||||
|
||||
@spoiler.setter
|
||||
def spoiler(self, spoiler: bool) -> None:
|
||||
self.underlying.spoiler = spoiler
|
||||
|
||||
@property
|
||||
def colour(self) -> Colour | None:
|
||||
return self.underlying.accent_color
|
||||
|
||||
@colour.setter
|
||||
def colour(self, value: int | Colour | None): # type: ignore
|
||||
self.underlying.accent_color = Colour.resolve_value(value)
|
||||
|
||||
color = colour
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return any(item.is_dispatchable() for item in self.items)
|
||||
|
||||
def is_persistent(self) -> bool:
|
||||
return all(item.is_persistent() for item in self.items)
|
||||
|
||||
def refresh_component(self, component: ContainerComponent) -> None:
|
||||
self.underlying = component
|
||||
i = 0
|
||||
for y in component.components:
|
||||
x = self.items[i]
|
||||
x.refresh_component(y)
|
||||
i += 1
|
||||
|
||||
def disable_all_items(self, *, exclusions: list[ViewItem] | None = None) -> Self:
|
||||
"""
|
||||
Disables all buttons and select menus in the container.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exclusions: Optional[List[:class:`ViewItem`]]
|
||||
A list of items in `self.items` to not disable from the view.
|
||||
"""
|
||||
for item in self.walk_items():
|
||||
if hasattr(item, "disabled") and (
|
||||
exclusions is None or item not in exclusions
|
||||
):
|
||||
item.disabled = True
|
||||
return self
|
||||
|
||||
def enable_all_items(self, *, exclusions: list[ViewItem] | None = None) -> Self:
|
||||
"""
|
||||
Enables all buttons and select menus in the container.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exclusions: Optional[List[:class:`ViewItem`]]
|
||||
A list of items in `self.items` to not enable from the view.
|
||||
"""
|
||||
for item in self.walk_items():
|
||||
if hasattr(item, "disabled") and (
|
||||
exclusions is None or item not in exclusions
|
||||
):
|
||||
item.disabled = False
|
||||
return self
|
||||
|
||||
def walk_items(self) -> Iterator[ViewItem]:
|
||||
for item in self.items:
|
||||
if hasattr(item, "walk_items"):
|
||||
yield from item.walk_items()
|
||||
else:
|
||||
yield item
|
||||
|
||||
def to_component_dict(self) -> ContainerComponentPayload:
|
||||
self._underlying = self._generate_underlying()
|
||||
return super().to_component_dict()
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[C], component: ContainerComponent) -> C:
|
||||
from .view import _component_to_item
|
||||
|
||||
items = [_component_to_item(c) for c in component.components]
|
||||
return cls(
|
||||
*items,
|
||||
colour=component.accent_color,
|
||||
spoiler=component.spoiler,
|
||||
id=component.id,
|
||||
)
|
||||
|
||||
callback = None
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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 time
|
||||
from itertools import groupby
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from ..utils import find, get
|
||||
from .item import Item, ItemCallbackType
|
||||
|
||||
__all__ = ("ItemInterface",)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .view import View
|
||||
|
||||
|
||||
class ItemInterface:
|
||||
"""The base structure for classes that contain :class:`~discord.ui.Item`.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*items: :class:`Item`
|
||||
The initial items contained in this structure.
|
||||
timeout: Optional[:class:`float`]
|
||||
Timeout in seconds from last interaction with the UI before no longer accepting input. Defaults to 180.0.
|
||||
If ``None`` then there is no timeout.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
timeout: Optional[:class:`float`]
|
||||
Timeout from last interaction with the UI before no longer accepting input.
|
||||
If ``None`` then there is no timeout.
|
||||
children: List[:class:`Item`]
|
||||
The list of children attached to this structure.
|
||||
store: Optional[:class:`bool`]
|
||||
Whether this interface should be stored for callback listening. Setting it to ``False`` will ignore callbacks and prevent item values from being refreshed. Defaults to ``True``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*items: Item,
|
||||
timeout: float | None = 180.0,
|
||||
store: bool = True,
|
||||
):
|
||||
self.timeout: float | None = timeout
|
||||
self.children: list[Item] = []
|
||||
for item in items:
|
||||
self.add_item(item)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
self._cancel_callback: Callable[[View], None] | None = None
|
||||
self._timeout_expiry: float | None = None
|
||||
self._timeout_task: asyncio.Task[None] | None = None
|
||||
self._stopped: asyncio.Future[bool] = loop.create_future()
|
||||
self._store = store
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} timeout={self.timeout} children={len(self.children)}>"
|
||||
|
||||
async def _timeout_task_impl(self) -> None:
|
||||
while True:
|
||||
# Guard just in case someone changes the value of the timeout at runtime
|
||||
if self.timeout is None:
|
||||
return
|
||||
|
||||
if self._timeout_expiry is None:
|
||||
return self._dispatch_timeout()
|
||||
|
||||
# Check if we've elapsed our currently set timeout
|
||||
now = time.monotonic()
|
||||
if now >= self._timeout_expiry:
|
||||
return self._dispatch_timeout()
|
||||
|
||||
# Wait N seconds to see if timeout data has been refreshed
|
||||
await asyncio.sleep(self._timeout_expiry - now)
|
||||
|
||||
@property
|
||||
def _expires_at(self) -> float | None:
|
||||
if self.timeout:
|
||||
return time.monotonic() + self.timeout
|
||||
return None
|
||||
|
||||
def _dispatch_timeout(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_components(self) -> list[dict[str, Any]]:
|
||||
return [item.to_component_dict() for item in self.children]
|
||||
|
||||
def get_item(self, custom_id: str | int) -> Item | None:
|
||||
"""Gets an item from this structure. Roughly equal to `utils.get(self.children, ...)`.
|
||||
If an :class:`int` is provided, the item will be retrieved by ``id``, otherwise by ``custom_id``.
|
||||
This method will also search nested items.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_id: Union[:class:`str`, :class:`int`]
|
||||
The id of the item to get
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`Item`]
|
||||
The item with the matching ``custom_id`` or ``id`` if it exists.
|
||||
"""
|
||||
if not custom_id:
|
||||
return None
|
||||
attr = "id" if isinstance(custom_id, int) else "custom_id"
|
||||
child = find(lambda i: getattr(i, attr, None) == custom_id, self.children)
|
||||
if not child:
|
||||
for i in self.children:
|
||||
if hasattr(i, "get_item"):
|
||||
if child := i.get_item(custom_id):
|
||||
return child
|
||||
return child
|
||||
|
||||
def add_item(self, item: Item) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
def remove_item(self, item: Item) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
def clear_items(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
A callback that is called when this structure's timeout elapses without being explicitly stopped.
|
||||
"""
|
||||
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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, TypeVar
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ..components import FileComponent, UnfurledMediaItem, _component_factory
|
||||
from ..enums import ComponentType
|
||||
from .item import ViewItem
|
||||
|
||||
__all__ = ("File",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..types.components import FileComponent as FileComponentPayload
|
||||
from .view import DesignerView
|
||||
|
||||
|
||||
F = TypeVar("F", bound="File")
|
||||
V = TypeVar("V", bound="DesignerView", covariant=True)
|
||||
|
||||
|
||||
class File(ViewItem[V]):
|
||||
"""Represents a UI File.
|
||||
|
||||
.. note::
|
||||
This component does not show media previews. Use :class:`MediaGallery` for previews instead.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url: :class:`str`
|
||||
The URL of this file. This must be an ``attachment://`` URL referring to a local file used with :class:`~discord.File`.
|
||||
spoiler: Optional[:class:`bool`]
|
||||
Whether this file has the spoiler overlay.
|
||||
id: Optional[:class:`int`]
|
||||
The file component's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"file",
|
||||
"spoiler",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(self, url: str, *, spoiler: bool = False, id: int | None = None):
|
||||
super().__init__()
|
||||
|
||||
file = UnfurledMediaItem(url)
|
||||
|
||||
self._underlying = self._generate_underlying(
|
||||
id=id,
|
||||
file=file,
|
||||
spoiler=spoiler,
|
||||
)
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
file: UnfurledMediaItem | None = None,
|
||||
spoiler: bool | None = None,
|
||||
id: int | None = None,
|
||||
) -> FileComponent:
|
||||
super()._generate_underlying(FileComponent)
|
||||
return FileComponent._raw_construct(
|
||||
type=ComponentType.file,
|
||||
id=id or self.id,
|
||||
file=file or self.file,
|
||||
spoiler=spoiler if spoiler is not None else self.spoiler,
|
||||
)
|
||||
|
||||
@property
|
||||
def file(self) -> UnfurledMediaItem:
|
||||
"""The file's unerlying media item."""
|
||||
return self.underlying.file
|
||||
|
||||
@file.setter
|
||||
def file(self, value: UnfurledMediaItem) -> None:
|
||||
self.underlying.file = value
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""The URL of this file's underlying media. This must be an ``attachment://`` URL that references a :class:`~discord.File`."""
|
||||
return self.underlying.file and self.underlying.file.url
|
||||
|
||||
@url.setter
|
||||
def url(self, value: str) -> None:
|
||||
self.underlying.file.url = value
|
||||
|
||||
@property
|
||||
def spoiler(self) -> bool:
|
||||
"""Whether the file has the spoiler overlay. Defaults to ``False``."""
|
||||
return self.underlying.spoiler
|
||||
|
||||
@spoiler.setter
|
||||
def spoiler(self, spoiler: bool) -> None:
|
||||
self.underlying.spoiler = spoiler
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
"""The name of this file, if provided by Discord."""
|
||||
return self.underlying.name
|
||||
|
||||
@property
|
||||
def size(self) -> int | None:
|
||||
"""The size of this file in bytes, if provided by Discord."""
|
||||
return self.underlying.size
|
||||
|
||||
def refresh_component(self, component: FileComponent) -> None:
|
||||
original = self.underlying.file
|
||||
component.file._static_url = original._static_url
|
||||
self.underlying = component
|
||||
|
||||
def to_component_dict(self) -> FileComponentPayload:
|
||||
return super().to_component_dict()
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[F], component: FileComponent) -> F:
|
||||
url = component.file and component.file.url
|
||||
if not url.startswith("attachment://"):
|
||||
url = "attachment://" + urlparse(url).path.rsplit("/", 1)[-1]
|
||||
return cls(
|
||||
url,
|
||||
spoiler=component.spoiler,
|
||||
id=component.id,
|
||||
)
|
||||
|
||||
callback = None
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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
|
||||
|
||||
from ..components import FileUpload as FileUploadComponent
|
||||
from ..enums import ComponentType
|
||||
from ..message import Attachment
|
||||
from .item import ModalItem
|
||||
|
||||
__all__ = ("FileUpload",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from ..types.components import FileUploadComponent as FileUploadComponentPayload
|
||||
|
||||
|
||||
class FileUpload(ModalItem):
|
||||
"""Represents a UI File Upload component.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the file upload field that gets received during an interaction.
|
||||
min_values: Optional[:class:`int`]
|
||||
The minimum number of files that must be uploaded.
|
||||
Defaults to 0 and must be between 0 and 10, inclusive.
|
||||
max_values: Optional[:class:`int`]
|
||||
The maximum number of files that can be uploaded.
|
||||
Must be between 1 and 10, inclusive.
|
||||
required: Optional[:class:`bool`]
|
||||
Whether the file upload field is required or not. Defaults to ``True``.
|
||||
id: Optional[:class:`int`]
|
||||
The file upload field's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"required",
|
||||
"min_values",
|
||||
"max_values",
|
||||
"custom_id",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
min_values: int | None = None,
|
||||
max_values: int | None = None,
|
||||
required: bool = True,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
if min_values and (min_values < 0 or min_values > 10):
|
||||
raise ValueError("min_values must be between 0 and 10")
|
||||
if max_values and (max_values < 1 or max_values > 10):
|
||||
raise ValueError("max_values must be between 1 and 10")
|
||||
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 not isinstance(required, bool):
|
||||
raise TypeError(f"required must be bool not {required.__class__.__name__}") # type: ignore
|
||||
custom_id = os.urandom(16).hex() if custom_id is None else custom_id
|
||||
self._attachments: list[Attachment] | None = None
|
||||
|
||||
self._underlying: FileUploadComponent = self._generate_underlying(
|
||||
custom_id=custom_id,
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
required=required,
|
||||
id=id,
|
||||
)
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
custom_id: str | None = None,
|
||||
min_values: int | None = None,
|
||||
max_values: int | None = None,
|
||||
required: bool | None = None,
|
||||
id: int | None = None,
|
||||
) -> FileUploadComponent:
|
||||
super()._generate_underlying(FileUploadComponent)
|
||||
return FileUploadComponent._raw_construct(
|
||||
type=ComponentType.file_upload,
|
||||
custom_id=custom_id or self.custom_id,
|
||||
min_values=min_values if min_values is not None else self.min_values,
|
||||
max_values=max_values if max_values is not None else self.max_values,
|
||||
required=required if required is not None else self.required,
|
||||
id=id or self.id,
|
||||
)
|
||||
|
||||
@property
|
||||
def custom_id(self) -> str:
|
||||
"""The custom id that gets received during an interaction."""
|
||||
return self.underlying.custom_id
|
||||
|
||||
@custom_id.setter
|
||||
def custom_id(self, value: str):
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"custom_id must be str not {value.__class__.__name__}")
|
||||
if len(value) > 100:
|
||||
raise ValueError("custom_id must be 100 characters or fewer")
|
||||
self.underlying.custom_id = value
|
||||
|
||||
@property
|
||||
def min_values(self) -> int | None:
|
||||
"""The minimum number of files that must be uploaded. Defaults to 0."""
|
||||
return self.underlying.min_values
|
||||
|
||||
@min_values.setter
|
||||
def min_values(self, value: int | None):
|
||||
if value and not isinstance(value, int):
|
||||
raise TypeError(f"min_values must be None or int not {value.__class__.__name__}") # type: ignore
|
||||
if value and (value < 0 or value > 10):
|
||||
raise ValueError("min_values must be between 0 and 10")
|
||||
self.underlying.min_values = value
|
||||
|
||||
@property
|
||||
def max_values(self) -> int | None:
|
||||
"""The maximum number of files that can be uploaded."""
|
||||
return self.underlying.max_values
|
||||
|
||||
@max_values.setter
|
||||
def max_values(self, value: int | None):
|
||||
if value and not isinstance(value, int):
|
||||
raise TypeError(f"max_values must be None or int not {value.__class__.__name__}") # type: ignore
|
||||
if value and (value < 1 or value > 10):
|
||||
raise ValueError("max_values must be between 1 and 10")
|
||||
self.underlying.max_values = value
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
"""Whether the input file upload is required or not. Defaults to ``True``."""
|
||||
return self.underlying.required
|
||||
|
||||
@required.setter
|
||||
def required(self, value: bool):
|
||||
if not isinstance(value, bool):
|
||||
raise TypeError(f"required must be bool not {value.__class__.__name__}") # type: ignore
|
||||
self.underlying.required = bool(value)
|
||||
|
||||
@property
|
||||
def values(self) -> list[Attachment] | None:
|
||||
"""The files that were uploaded to the field. This will be ``None`` if the file upload has not been submitted via a modal yet."""
|
||||
return self._attachments
|
||||
|
||||
def to_component_dict(self) -> FileUploadComponentPayload:
|
||||
return self.underlying.to_dict()
|
||||
|
||||
def refresh_from_modal(self, interaction: Interaction, data: dict) -> None:
|
||||
values = data.get("values", [])
|
||||
self._attachments = [
|
||||
Attachment(
|
||||
state=interaction._state,
|
||||
data=interaction.data["resolved"]["attachments"][attachment_id],
|
||||
)
|
||||
for attachment_id in values
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_component(
|
||||
cls: type[FileUpload], component: FileUploadComponent
|
||||
) -> FileUpload:
|
||||
|
||||
return cls(
|
||||
custom_id=component.custom_id,
|
||||
min_values=component.min_values,
|
||||
max_values=component.max_values,
|
||||
required=component.required,
|
||||
id=component.id,
|
||||
)
|
||||
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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
|
||||
|
||||
from ..components import InputText as InputTextComponent
|
||||
from ..enums import ComponentType, InputTextStyle
|
||||
from .item import ModalItem
|
||||
|
||||
__all__ = ("InputText", "TextInput")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from ..types.components import InputText as InputTextComponentPayload
|
||||
|
||||
|
||||
class InputText(ModalItem):
|
||||
"""Represents a UI text input field.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
style: :class:`~discord.InputTextStyle`
|
||||
The style of the input text field.
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the input text field that gets received during an interaction.
|
||||
label: :class:`str`
|
||||
The label for the input text field.
|
||||
Must be 45 characters or fewer.
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text that is shown if nothing is selected, if any.
|
||||
Must be 100 characters or fewer.
|
||||
min_length: Optional[:class:`int`]
|
||||
The minimum number of characters that must be entered.
|
||||
Defaults to 0 and must be less than 4000.
|
||||
max_length: Optional[:class:`int`]
|
||||
The maximum number of characters that can be entered.
|
||||
Must be between 1 and 4000.
|
||||
required: Optional[:class:`bool`]
|
||||
Whether the input text field is required or not. Defaults to ``True``.
|
||||
value: Optional[:class:`str`]
|
||||
Pre-fills the input text field with this value.
|
||||
Must be 4000 characters or fewer.
|
||||
row: Optional[:class:`int`]
|
||||
The relative row this input text field belongs to. A modal dialog can only have 5
|
||||
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"label",
|
||||
"placeholder",
|
||||
"value",
|
||||
"required",
|
||||
"style",
|
||||
"min_length",
|
||||
"max_length",
|
||||
"custom_id",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
style: InputTextStyle = InputTextStyle.short,
|
||||
custom_id: str | None = None,
|
||||
label: str | None = None,
|
||||
placeholder: str | None = None,
|
||||
min_length: int | None = None,
|
||||
max_length: int | None = None,
|
||||
required: bool | None = True,
|
||||
value: str | None = None,
|
||||
row: int | None = None,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
if label and len(str(label)) > 45:
|
||||
raise ValueError("label must be 45 characters or fewer")
|
||||
if min_length and (min_length < 0 or min_length > 4000):
|
||||
raise ValueError("min_length must be between 0 and 4000")
|
||||
if max_length and (max_length < 0 or max_length > 4000):
|
||||
raise ValueError("max_length must be between 1 and 4000")
|
||||
if value and len(str(value)) > 4000:
|
||||
raise ValueError("value must be 4000 characters or fewer")
|
||||
if placeholder and len(str(placeholder)) > 100:
|
||||
raise ValueError("placeholder must be 100 characters or fewer")
|
||||
if not isinstance(custom_id, str) and custom_id is not None:
|
||||
raise TypeError(
|
||||
f"expected custom_id to be str, not {custom_id.__class__.__name__}"
|
||||
)
|
||||
custom_id = os.urandom(16).hex() if custom_id is None else custom_id
|
||||
self._input_value = False
|
||||
self.row = row
|
||||
self._rendered_row: int | None = None
|
||||
|
||||
self._underlying = self._generate_underlying(
|
||||
style=style,
|
||||
custom_id=custom_id,
|
||||
label=label,
|
||||
placeholder=placeholder,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
required=required,
|
||||
value=value,
|
||||
id=id,
|
||||
)
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
style: InputTextStyle | None = None,
|
||||
custom_id: str | None = None,
|
||||
label: str | None = None,
|
||||
placeholder: str | None = None,
|
||||
min_length: int | None = None,
|
||||
max_length: int | None = None,
|
||||
required: bool | None = None,
|
||||
value: str | None = None,
|
||||
id: int | None = None,
|
||||
) -> InputTextComponent:
|
||||
super()._generate_underlying(InputTextComponent)
|
||||
return InputTextComponent._raw_construct(
|
||||
type=ComponentType.input_text,
|
||||
style=style or self.style,
|
||||
custom_id=custom_id or self.custom_id,
|
||||
label=label or self.label,
|
||||
placeholder=placeholder or self.placeholder,
|
||||
min_length=min_length if min_length is not None else self.min_length,
|
||||
max_length=max_length if max_length is not None else self.max_length,
|
||||
required=required if required is not None else self.required,
|
||||
value=value or self.value,
|
||||
id=id or self.id,
|
||||
)
|
||||
|
||||
@property
|
||||
def style(self) -> InputTextStyle:
|
||||
"""The style of the input text field."""
|
||||
return self.underlying.style
|
||||
|
||||
@style.setter
|
||||
def style(self, value: InputTextStyle):
|
||||
if not isinstance(value, InputTextStyle):
|
||||
raise TypeError(
|
||||
f"style must be of type InputTextStyle not {value.__class__.__name__}"
|
||||
)
|
||||
self.underlying.style = value
|
||||
|
||||
@property
|
||||
def custom_id(self) -> str:
|
||||
"""The ID of the input text field that gets received during an interaction."""
|
||||
return self.underlying.custom_id
|
||||
|
||||
@custom_id.setter
|
||||
def custom_id(self, value: str):
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"custom_id must be str not {value.__class__.__name__}")
|
||||
if len(value) > 100:
|
||||
raise ValueError("custom_id must be 100 characters or fewer")
|
||||
self.underlying.custom_id = value
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
"""The label of the input text field."""
|
||||
return self.underlying.label
|
||||
|
||||
@label.setter
|
||||
def label(self, value: str):
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"label should be str not {value.__class__.__name__}")
|
||||
if len(value) > 45:
|
||||
raise ValueError("label must be 45 characters or fewer")
|
||||
self.underlying.label = value
|
||||
|
||||
@property
|
||||
def placeholder(self) -> str | None:
|
||||
"""The placeholder text that is shown before anything is entered, if any."""
|
||||
return self.underlying.placeholder
|
||||
|
||||
@placeholder.setter
|
||||
def placeholder(self, value: str | None):
|
||||
if value and not isinstance(value, str):
|
||||
raise TypeError(f"placeholder must be None or str not {value.__class__.__name__}") # type: ignore
|
||||
if value and len(value) > 100:
|
||||
raise ValueError("placeholder must be 100 characters or fewer")
|
||||
self.underlying.placeholder = value
|
||||
|
||||
@property
|
||||
def min_length(self) -> int | None:
|
||||
"""The minimum number of characters that must be entered. Defaults to 0."""
|
||||
return self.underlying.min_length
|
||||
|
||||
@min_length.setter
|
||||
def min_length(self, value: int | None):
|
||||
if value and not isinstance(value, int):
|
||||
raise TypeError(f"min_length must be None or int not {value.__class__.__name__}") # type: ignore
|
||||
if value and (value < 0 or value) > 4000:
|
||||
raise ValueError("min_length must be between 0 and 4000")
|
||||
self.underlying.min_length = value
|
||||
|
||||
@property
|
||||
def max_length(self) -> int | None:
|
||||
"""The maximum number of characters that can be entered."""
|
||||
return self.underlying.max_length
|
||||
|
||||
@max_length.setter
|
||||
def max_length(self, value: int | None):
|
||||
if value and not isinstance(value, int):
|
||||
raise TypeError(f"min_length must be None or int not {value.__class__.__name__}") # type: ignore
|
||||
if value and (value <= 0 or value > 4000):
|
||||
raise ValueError("max_length must be between 1 and 4000")
|
||||
self.underlying.max_length = value
|
||||
|
||||
@property
|
||||
def required(self) -> bool | None:
|
||||
"""Whether the input text field is required or not. Defaults to ``True``."""
|
||||
return self.underlying.required
|
||||
|
||||
@required.setter
|
||||
def required(self, value: bool | None):
|
||||
if not isinstance(value, bool):
|
||||
raise TypeError(f"required must be bool not {value.__class__.__name__}") # type: ignore
|
||||
self.underlying.required = bool(value)
|
||||
|
||||
@property
|
||||
def value(self) -> str | None:
|
||||
"""The value entered in the text field."""
|
||||
if self._input_value is not False:
|
||||
# only False on init, otherwise the value was either set or cleared
|
||||
return self._input_value # type: ignore
|
||||
return self.underlying.value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: str | None):
|
||||
if value and not isinstance(value, str):
|
||||
raise TypeError(f"value must be None or str not {value.__class__.__name__}") # type: ignore
|
||||
if value and len(str(value)) > 4000:
|
||||
raise ValueError("value must be 4000 characters or fewer")
|
||||
self.underlying.value = value
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return 5
|
||||
|
||||
def to_component_dict(self) -> InputTextComponentPayload:
|
||||
return super().to_component_dict()
|
||||
|
||||
def refresh_state(self, data) -> None:
|
||||
self._input_value = data.get("value", None)
|
||||
|
||||
def refresh_from_modal(
|
||||
self, interaction: Interaction, data: InputTextComponentPayload
|
||||
) -> None:
|
||||
return self.refresh_state(data)
|
||||
|
||||
@classmethod
|
||||
def from_component(
|
||||
cls: type[InputText], component: InputTextComponent
|
||||
) -> InputText:
|
||||
|
||||
return cls(
|
||||
style=component.style,
|
||||
custom_id=component.custom_id,
|
||||
label=component.label,
|
||||
placeholder=component.placeholder,
|
||||
min_length=component.min_length,
|
||||
max_length=component.max_length,
|
||||
required=component.required,
|
||||
value=component.value,
|
||||
id=component.id,
|
||||
)
|
||||
|
||||
|
||||
TextInput = InputText
|
||||
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2021 Rapptz
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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, Coroutine, Generic, TypeVar
|
||||
|
||||
from ..interactions import Interaction
|
||||
|
||||
__all__ = (
|
||||
"Item",
|
||||
"ViewItem",
|
||||
"ModalItem",
|
||||
)
|
||||
|
||||
from ..utils import warn_deprecated
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..components import Component
|
||||
from ..enums import ComponentType
|
||||
from .core import ItemInterface
|
||||
from .modal import BaseModal
|
||||
from .view import BaseView
|
||||
|
||||
I = TypeVar("I", bound="Item")
|
||||
T = TypeVar("T", bound="ItemInterface", covariant=True)
|
||||
V = TypeVar("V", bound="BaseView", covariant=True)
|
||||
M = TypeVar("M", bound="BaseModal", covariant=True)
|
||||
ItemCallbackType = Callable[[Any, I, Interaction], Coroutine[Any, Any, Any]]
|
||||
|
||||
|
||||
class Item(Generic[T]):
|
||||
"""Represents the base UI item that all UI components inherit from.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
Now used as base class for :class:`ViewItem` and :class:`ModalItem`.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = ("id",)
|
||||
|
||||
def __init__(self):
|
||||
self._underlying: Component | None = None
|
||||
self._provided_custom_id: bool = False
|
||||
self.parent: Item | ItemInterface | None = None
|
||||
|
||||
def to_component_dict(self) -> dict[str, Any]:
|
||||
if not self.underlying:
|
||||
raise NotImplementedError
|
||||
return self.underlying.to_dict()
|
||||
|
||||
def refresh_component(self, component: Component) -> None:
|
||||
self.underlying = component
|
||||
|
||||
def refresh_state(self, interaction: Interaction) -> None:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[I], component: Component) -> I:
|
||||
return cls()
|
||||
|
||||
@property
|
||||
def underlying(self) -> Component:
|
||||
return self._underlying
|
||||
|
||||
@underlying.setter
|
||||
def underlying(self, value: Component) -> None:
|
||||
self._underlying = value
|
||||
|
||||
@property
|
||||
def type(self) -> ComponentType:
|
||||
"""The underlying component's type."""
|
||||
if not self.underlying:
|
||||
raise NotImplementedError
|
||||
return self.underlying.type
|
||||
|
||||
def _generate_underlying(self, cls: type[Component]) -> Component:
|
||||
if not self._underlying:
|
||||
self._underlying = cls._raw_construct()
|
||||
return self._underlying
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return False
|
||||
|
||||
def is_storable(self) -> bool:
|
||||
return False
|
||||
|
||||
def is_persistent(self) -> bool:
|
||||
return not self.is_dispatchable() or self._provided_custom_id
|
||||
|
||||
def copy_text(self) -> str:
|
||||
return ""
|
||||
|
||||
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 id(self) -> int | None:
|
||||
"""Gets this item's ID.
|
||||
|
||||
This can be set by the user when constructing an Item. If not, Discord will automatically provide one when the item's parent is sent.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`int`]
|
||||
The ID of this item, or ``None`` if the user didn't set one.
|
||||
"""
|
||||
return self.underlying and self.underlying.id
|
||||
|
||||
@id.setter
|
||||
def id(self, value) -> None:
|
||||
if not self.underlying:
|
||||
return
|
||||
self.underlying.id = value
|
||||
|
||||
|
||||
class ViewItem(Item[V]):
|
||||
"""Represents an item used in Views.
|
||||
|
||||
The following are the original items supported in :class:`discord.ui.View`:
|
||||
|
||||
- :class:`discord.ui.Button`
|
||||
- :class:`discord.ui.Select`
|
||||
|
||||
And the following are new items under the "Components V2" specification for use in :class:`discord.ui.DesignerView`:
|
||||
|
||||
- :class:`discord.ui.Section`
|
||||
- :class:`discord.ui.TextDisplay`
|
||||
- :class:`discord.ui.Thumbnail`
|
||||
- :class:`discord.ui.MediaGallery`
|
||||
- :class:`discord.ui.File`
|
||||
- :class:`discord.ui.Separator`
|
||||
- :class:`discord.ui.Container`
|
||||
|
||||
Additionally, :class:`discord.ui.ActionRow` should be used in :class:`discord.ui.DesignerView` to support :class:`discord.ui.Button` and :class:`discord.ui.Select`.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._view: V | None = None
|
||||
self.parent: ViewItem | BaseView | None = None
|
||||
|
||||
@property
|
||||
def row(self) -> int | None:
|
||||
warn_deprecated("Accessing .row from CV2 Items", since="2.7.1", removed="3.0")
|
||||
return None
|
||||
|
||||
@row.setter
|
||||
def row(self, value: int | None) -> None:
|
||||
warn_deprecated("Setting .row on CV2 Items", since="2.7.1", removed="3.0")
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return 1
|
||||
|
||||
@property
|
||||
def view(self) -> V | None:
|
||||
"""Gets the parent view associated with this item.
|
||||
|
||||
The view refers to the structure that holds this item. This is typically set
|
||||
automatically when the item is added to a view.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`BaseView`]
|
||||
The parent view of this item, or ``None`` if the item is not attached to any view.
|
||||
"""
|
||||
if self._view:
|
||||
return self._view
|
||||
if self.parent:
|
||||
from .view import BaseView
|
||||
|
||||
if isinstance(self.parent, BaseView):
|
||||
return self.parent
|
||||
return self.parent.view
|
||||
return None
|
||||
|
||||
@view.setter
|
||||
def view(self, value: V | None) -> None:
|
||||
warn_deprecated("Manually setting .view", since="2.7", removed="3.0")
|
||||
self._view = value
|
||||
|
||||
async def callback(self, interaction: Interaction):
|
||||
"""|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.
|
||||
"""
|
||||
|
||||
|
||||
class ModalItem(Item[M]):
|
||||
"""Represents an item used in Modals.
|
||||
|
||||
:class:`discord.ui.InputText` is the original item supported in :class:`discord.ui.Modal`.
|
||||
|
||||
The following are newly available in :class:`discord.ui.DesignerModal`:
|
||||
|
||||
- :class:`discord.ui.Label`
|
||||
- :class:`discord.ui.TextDisplay`
|
||||
|
||||
And :class:`discord.ui.Label` should be used in :class:`discord.ui.DesignerModal` to add the following items:
|
||||
|
||||
- :class:`discord.ui.InputText`
|
||||
- :class:`discord.ui.Select`
|
||||
- :class:`discord.ui.FileUpload`
|
||||
- :class:`discord.ui.RadioGroup`
|
||||
- :class:`discord.ui.CheckboxGroup`
|
||||
- :class:`discord.ui.Checkbox`
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._modal: M | None = None
|
||||
self.parent: ModalItem | BaseModal | None = self.modal
|
||||
|
||||
def refresh_from_modal(self, interaction: Interaction, data: dict) -> None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def modal(self) -> M | None:
|
||||
"""Gets the parent modal associated with this item. This is typically set
|
||||
automatically when the item is added to a modal.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`BaseModal`]
|
||||
The parent modal of this item, or ``None`` if the item is not attached to any modal.
|
||||
"""
|
||||
return self._modal
|
||||
|
||||
@modal.setter
|
||||
def modal(self, value) -> None:
|
||||
self._modal = value
|
||||
@@ -0,0 +1,566 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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 collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Iterator, Literal, TypeVar, overload
|
||||
|
||||
from ..components import (
|
||||
CheckboxGroupOption,
|
||||
)
|
||||
from ..components import Label as LabelComponent
|
||||
from ..components import (
|
||||
RadioGroupOption,
|
||||
SelectDefaultValue,
|
||||
SelectOption,
|
||||
_component_factory,
|
||||
)
|
||||
from ..enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle
|
||||
from ..utils import find, get
|
||||
from .button import Button
|
||||
from .checkbox import Checkbox
|
||||
from .checkbox_group import CheckboxGroup
|
||||
from .file_upload import FileUpload
|
||||
from .input_text import InputText
|
||||
from .item import ItemCallbackType, ModalItem
|
||||
from .radio_group import RadioGroup
|
||||
from .select import Select
|
||||
|
||||
__all__ = ("Label",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..emoji import AppEmoji, GuildEmoji
|
||||
from ..interactions import Interaction
|
||||
from ..partial_emoji import PartialEmoji, _EmojiTag
|
||||
from ..types.components import LabelComponent as LabelComponentPayload
|
||||
from .modal import DesignerModal
|
||||
|
||||
|
||||
L = TypeVar("L", bound="Label")
|
||||
M = TypeVar("M", bound="DesignerModal", covariant=True)
|
||||
|
||||
|
||||
class Label(ModalItem[M]):
|
||||
"""Represents a UI Label used in :class:`discord.ui.DesignerModal`.
|
||||
|
||||
The items currently supported are as follows:
|
||||
|
||||
- :class:`discord.ui.Select`
|
||||
- :class:`discord.ui.InputText`
|
||||
- :class:`discord.ui.FileUpload`
|
||||
- :class:`discord.ui.RadioGroup`
|
||||
- :class:`discord.ui.CheckboxGroup`
|
||||
- :class:`discord.ui.Checkbox`
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`ModalItem`
|
||||
The initial item attached to this label.
|
||||
label: :class:`str`
|
||||
The label text. Must be 45 characters or fewer.
|
||||
description: Optional[:class:`str`]
|
||||
The description for this label. Must be 100 characters or fewer.
|
||||
id: Optional[:class:`int`]
|
||||
The label's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
item: :class:`ViewItem`
|
||||
The label's attached item.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"item",
|
||||
"id",
|
||||
"label",
|
||||
"description",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
label: str,
|
||||
item: ModalItem = None,
|
||||
*,
|
||||
description: str | None = None,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.item: ModalItem = None
|
||||
|
||||
self._underlying = self._generate_underlying(
|
||||
id=id,
|
||||
label=label,
|
||||
description=description,
|
||||
)
|
||||
|
||||
if item:
|
||||
self.set_item(item)
|
||||
|
||||
@ModalItem.modal.setter
|
||||
def modal(self, value):
|
||||
self._modal = value
|
||||
if self.item:
|
||||
self.item.modal = value
|
||||
|
||||
def _set_component_from_item(self, item: ModalItem):
|
||||
self.underlying.component = item._generate_underlying()
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
label: str | None = None,
|
||||
description: str | None = None,
|
||||
id: int | None = None,
|
||||
) -> LabelComponent:
|
||||
super()._generate_underlying(LabelComponent)
|
||||
label = LabelComponent._raw_construct(
|
||||
type=ComponentType.label,
|
||||
id=id or self.id,
|
||||
component=None,
|
||||
label=label or self.label,
|
||||
description=description or self.description,
|
||||
)
|
||||
|
||||
if self.item:
|
||||
label.component = self.item._generate_underlying()
|
||||
return label
|
||||
|
||||
def set_item(self, item: ModalItem) -> Self:
|
||||
"""Set this label's item.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: Union[:class:`ModalItem`]
|
||||
The item to set.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
A :class:`ModalItem` was not passed.
|
||||
"""
|
||||
|
||||
if not isinstance(item, ModalItem):
|
||||
raise TypeError(f"expected ModalItem not {item.__class__!r}")
|
||||
if isinstance(item, InputText) and item.label:
|
||||
raise ValueError(f"InputText.label cannot be set inside Label")
|
||||
if self.modal:
|
||||
item.modal = self.modal
|
||||
item.parent = self
|
||||
|
||||
self.item = item
|
||||
self._set_component_from_item(item)
|
||||
return self
|
||||
|
||||
def get_item(self, id: str | int) -> ModalItem | None:
|
||||
"""Get the item from this label if it matches the provided id.
|
||||
If an ``int`` is provided, the item will match by ``id``, otherwise by ``custom_id``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id: Union[:class:`str`, :class:`int`]
|
||||
The id or custom_id of the item to match.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`ModalItem`]
|
||||
The item if its ``id`` or ``custom_id`` matches.
|
||||
"""
|
||||
if not id:
|
||||
return None
|
||||
attr = "id" if isinstance(id, int) else "custom_id"
|
||||
if getattr(self.item, attr, None) != id:
|
||||
return None
|
||||
return self.item
|
||||
|
||||
def set_input_text(
|
||||
self,
|
||||
*,
|
||||
style: InputTextStyle = InputTextStyle.short,
|
||||
custom_id: str | None = None,
|
||||
placeholder: str | None = None,
|
||||
min_length: int | None = None,
|
||||
max_length: int | None = None,
|
||||
required: bool | None = True,
|
||||
value: str | None = None,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Set this label's item to an input text.
|
||||
|
||||
To set a pre-existing :class:`InputText`, use the
|
||||
:meth:`set_item` method, instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
style: :class:`~discord.InputTextStyle`
|
||||
The style of the input text field.
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the input text field that gets received during an interaction.
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text that is shown if nothing is selected, if any.
|
||||
Must be 100 characters or fewer.
|
||||
min_length: Optional[:class:`int`]
|
||||
The minimum number of characters that must be entered.
|
||||
Defaults to 0 and must be less than 4000.
|
||||
max_length: Optional[:class:`int`]
|
||||
The maximum number of characters that can be entered.
|
||||
Must be between 1 and 4000.
|
||||
required: Optional[:class:`bool`]
|
||||
Whether the input text field is required or not. Defaults to ``True``.
|
||||
value: Optional[:class:`str`]
|
||||
Pre-fills the input text field with this value.
|
||||
Must be 4000 characters or fewer.
|
||||
id: Optional[:class:`int`]
|
||||
The button's ID.
|
||||
"""
|
||||
|
||||
text = InputText(
|
||||
style=style,
|
||||
custom_id=custom_id,
|
||||
placeholder=placeholder,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
required=required,
|
||||
value=value,
|
||||
id=id,
|
||||
)
|
||||
|
||||
return self.set_item(text)
|
||||
|
||||
@overload
|
||||
def set_select(
|
||||
self,
|
||||
select_type: Literal[ComponentType.string_select] = ...,
|
||||
*,
|
||||
custom_id: str | None = ...,
|
||||
placeholder: str | None = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
options: list[SelectOption] | None = ...,
|
||||
required: bool = ...,
|
||||
id: int | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def set_select(
|
||||
self,
|
||||
select_type: Literal[ComponentType.channel_select] = ...,
|
||||
*,
|
||||
custom_id: str | None = ...,
|
||||
placeholder: str | None = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
channel_types: list[ChannelType] | None = ...,
|
||||
required: bool = ...,
|
||||
id: int | None = ...,
|
||||
default_values: Sequence[SelectDefaultValue] | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def set_select(
|
||||
self,
|
||||
select_type: Literal[
|
||||
ComponentType.user_select,
|
||||
ComponentType.role_select,
|
||||
ComponentType.mentionable_select,
|
||||
] = ...,
|
||||
*,
|
||||
custom_id: str | None = ...,
|
||||
placeholder: str | None = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
required: bool = ...,
|
||||
id: int | None = ...,
|
||||
default_values: Sequence[SelectDefaultValue] | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
def set_select(
|
||||
self,
|
||||
select_type: ComponentType = ComponentType.string_select,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
placeholder: str | None = None,
|
||||
min_values: int = 1,
|
||||
max_values: int = 1,
|
||||
options: list[SelectOption] | None = None,
|
||||
channel_types: list[ChannelType] | None = None,
|
||||
required: bool = True,
|
||||
id: int | None = None,
|
||||
default_values: Sequence[SelectDefaultValue] | None = None,
|
||||
) -> Self:
|
||||
"""Set this label's item to a select menu.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
select_type: :class:`discord.ComponentType`
|
||||
The type of select to create. Must be one of
|
||||
:attr:`discord.ComponentType.string_select`, :attr:`discord.ComponentType.user_select`,
|
||||
:attr:`discord.ComponentType.role_select`, :attr:`discord.ComponentType.mentionable_select`,
|
||||
or :attr:`discord.ComponentType.channel_select`.
|
||||
custom_id: :class:`str`
|
||||
The custom ID of the select menu that gets received during an interaction.
|
||||
If not given then one is generated for you.
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text that is shown if nothing is selected, if any.
|
||||
min_values: :class:`int`
|
||||
The minimum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
max_values: :class:`int`
|
||||
The maximum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
options: List[:class:`discord.SelectOption`]
|
||||
A list of options that can be selected in this menu.
|
||||
Only valid for selects of type :attr:`discord.ComponentType.string_select`.
|
||||
channel_types: List[:class:`discord.ChannelType`]
|
||||
A list of channel types that can be selected in this menu.
|
||||
Only valid for selects of type :attr:`discord.ComponentType.channel_select`.
|
||||
required: :class:`bool`
|
||||
Whether the select is required or not. Defaults to ``True``.
|
||||
id: Optional[:class:`int`]
|
||||
The select menu's ID.
|
||||
default_values: Optional[Sequence[Union[:class:`discord.SelectDefaultValue`, :class:`discord.abc.Snowflake`]]]
|
||||
The default values of this select. Only applicable if :attr:`.select_type` is not :attr:`discord.ComponentType.string_select`.
|
||||
|
||||
These can be either :class:`discord.SelectDefaultValue` instances or models, which will be converted into :class:`discord.SelectDefaultValue`
|
||||
instances.
|
||||
"""
|
||||
|
||||
select = Select(
|
||||
select_type=select_type,
|
||||
custom_id=custom_id,
|
||||
placeholder=placeholder,
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
options=options or [],
|
||||
channel_types=channel_types or [],
|
||||
required=required,
|
||||
id=id,
|
||||
default_values=default_values,
|
||||
)
|
||||
|
||||
return self.set_item(select)
|
||||
|
||||
def set_file_upload(
|
||||
self,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
min_values: int | None = None,
|
||||
max_values: int | None = None,
|
||||
required: bool | None = True,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Set this label's item to a file upload.
|
||||
|
||||
To set a pre-existing :class:`FileUpload`, use the
|
||||
:meth:`set_item` method, instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the input text field that gets received during an interaction.
|
||||
min_values: Optional[:class:`int`]
|
||||
The minimum number of files that must be uploaded.
|
||||
Defaults to 0 and must be between 0 and 10, inclusive.
|
||||
max_values: Optional[:class:`int`]
|
||||
The maximum number of files that can be uploaded.
|
||||
Must be between 1 and 10, inclusive.
|
||||
required: Optional[:class:`bool`]
|
||||
Whether the file upload field is required or not. Defaults to ``True``.
|
||||
id: Optional[:class:`int`]
|
||||
The file upload field's ID.
|
||||
"""
|
||||
|
||||
upload = FileUpload(
|
||||
custom_id=custom_id,
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
required=required,
|
||||
id=id,
|
||||
)
|
||||
|
||||
return self.set_item(upload)
|
||||
|
||||
def set_radio_group(
|
||||
self,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
options: list[RadioGroupOption] | None = None,
|
||||
required: bool | None = True,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Set this label's item to a radio group.
|
||||
|
||||
To set a pre-existing :class:`RadioGroup`, use the
|
||||
:meth:`set_item` method, instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the radio group that gets received during an interaction.
|
||||
options: List[:class:`discord.RadioGroupOption`]
|
||||
A list of options that can be selected from this group.
|
||||
required: Optional[:class:`bool`]
|
||||
Whether an option selection is required or not. Defaults to ``True``.
|
||||
id: Optional[:class:`int`]
|
||||
The radio group's ID.
|
||||
"""
|
||||
|
||||
radio = RadioGroup(
|
||||
custom_id=custom_id,
|
||||
option=options,
|
||||
required=required,
|
||||
id=id,
|
||||
)
|
||||
|
||||
return self.set_item(radio)
|
||||
|
||||
def set_checkbox_group(
|
||||
self,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
options: list[CheckboxGroupOption] | None = None,
|
||||
min_values: int | None = None,
|
||||
max_values: int | None = None,
|
||||
required: bool | None = True,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Set this label's item to a checkbox group.
|
||||
|
||||
To set a pre-existing :class:`CheckboxGroup`, use the
|
||||
:meth:`set_item` method, instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the checkbox group that gets received during an interaction.
|
||||
options: List[:class:`discord.CheckboxGroupOption`]
|
||||
A list of options that can be selected in this group.
|
||||
min_values: Optional[:class:`int`]
|
||||
The minimum number of options that must be selected.
|
||||
Defaults to 0 and must be between 0 and 10, inclusive.
|
||||
max_values: Optional[:class:`int`]
|
||||
The maximum number of options that can be selected.
|
||||
Must be between 1 and 10, inclusive.
|
||||
required: Optional[:class:`bool`]
|
||||
Whether an option selection is required or not. Defaults to ``True``.
|
||||
id: Optional[:class:`int`]
|
||||
The checkbox group's ID.
|
||||
"""
|
||||
|
||||
checkboxes = CheckboxGroup(
|
||||
custom_id=custom_id,
|
||||
option=options,
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
required=required,
|
||||
id=id,
|
||||
)
|
||||
|
||||
return self.set_item(checkboxes)
|
||||
|
||||
def set_checkbox(
|
||||
self,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
default: bool | None = False,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Set this label's item to a checkbox.
|
||||
|
||||
To set a pre-existing :class:`Checkbox`, use the
|
||||
:meth:`set_item` method, instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the checkbox that gets received during an interaction.
|
||||
default: Optional[:class:`bool`]
|
||||
Whether this checkbox is selected by default or not.
|
||||
id: Optional[:class:`int`]
|
||||
The checkbox's ID.
|
||||
"""
|
||||
|
||||
checkbox = Checkbox(
|
||||
custom_id=custom_id,
|
||||
default=default,
|
||||
id=id,
|
||||
)
|
||||
|
||||
return self.set_item(checkbox)
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
"""The label text. Must be 45 characters or fewer."""
|
||||
return self.underlying.label
|
||||
|
||||
@label.setter
|
||||
def label(self, value: str) -> None:
|
||||
self.underlying.label = value
|
||||
|
||||
@property
|
||||
def description(self) -> str | None:
|
||||
"""The description for this label. Must be 100 characters or fewer."""
|
||||
return self.underlying.description
|
||||
|
||||
@description.setter
|
||||
def description(self, value: str | None) -> None:
|
||||
self.underlying.description = value
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return self.item.is_dispatchable()
|
||||
|
||||
def is_persistent(self) -> bool:
|
||||
return self.item.is_persistent()
|
||||
|
||||
def refresh_component(self, component: LabelComponent) -> None:
|
||||
self.underlying = component
|
||||
self.item.refresh_component(component.component)
|
||||
|
||||
def walk_items(self) -> Iterator[ModalItem]:
|
||||
yield from [self.item]
|
||||
|
||||
def to_component_dict(self) -> LabelComponentPayload:
|
||||
self._set_component_from_item(self.item)
|
||||
return super().to_component_dict()
|
||||
|
||||
def refresh_from_modal(
|
||||
self, interaction: Interaction, data: LabelComponentPayload
|
||||
) -> None:
|
||||
return self.item.refresh_from_modal(interaction, data.get("component", {}))
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[L], component: LabelComponent) -> L:
|
||||
from .view import _component_to_item
|
||||
|
||||
item = _component_to_item(component.component)
|
||||
return cls(
|
||||
label=component.label,
|
||||
item=item,
|
||||
id=component.id,
|
||||
description=component.description,
|
||||
)
|
||||
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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, TypeVar
|
||||
|
||||
from ..components import MediaGallery as MediaGalleryComponent
|
||||
from ..components import MediaGalleryItem
|
||||
from ..enums import ComponentType
|
||||
from .item import ViewItem
|
||||
|
||||
__all__ = ("MediaGallery",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..types.components import MediaGalleryComponent as MediaGalleryComponentPayload
|
||||
from .view import DesignerView
|
||||
|
||||
|
||||
M = TypeVar("M", bound="MediaGallery")
|
||||
V = TypeVar("V", bound="DesignerView", covariant=True)
|
||||
|
||||
|
||||
class MediaGallery(ViewItem[V]):
|
||||
"""Represents a UI Media Gallery. Galleries may contain up to 10 :class:`MediaGalleryItem` objects.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*items: :class:`~discord.MediaGalleryItem`
|
||||
The initial items contained in this gallery, up to 10.
|
||||
id: Optional[:class:`int`]
|
||||
The gallery's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
items: List[:class:`~discord.MediaGalleryItem`]
|
||||
The list of media items in this gallery.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"items",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(self, *items: MediaGalleryItem, id: int | None = None):
|
||||
super().__init__()
|
||||
|
||||
self._underlying = self._generate_underlying(id=id, items=items)
|
||||
|
||||
def _generate_underlying(
|
||||
self, id: int | None = None, items: list[MediaGalleryItem] | None = None
|
||||
) -> MediaGalleryComponent:
|
||||
super()._generate_underlying(MediaGalleryComponent)
|
||||
return MediaGalleryComponent._raw_construct(
|
||||
type=ComponentType.media_gallery,
|
||||
id=id or self.id,
|
||||
items=[i for i in items] if items else [i for i in self.items or []],
|
||||
)
|
||||
|
||||
@property
|
||||
def items(self) -> list[MediaGalleryItem]:
|
||||
"""The list of media items in this gallery."""
|
||||
return self.underlying.items
|
||||
|
||||
@items.setter
|
||||
def items(self, value: list[MediaGalleryItem]) -> None:
|
||||
if len(value) > 10:
|
||||
raise ValueError("may not set more than 10 items in a gallery.")
|
||||
|
||||
if not all(isinstance(i, MediaGalleryItem) for i in value):
|
||||
raise TypeError(f"items must be a list of MediaGalleryItem.")
|
||||
|
||||
self.underlying.items = value
|
||||
|
||||
def append_item(self, item: MediaGalleryItem) -> Self:
|
||||
"""Adds a :attr:`MediaGalleryItem` to the gallery.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`MediaGalleryItem`
|
||||
The gallery 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.items) >= 10:
|
||||
raise ValueError("maximum number of items exceeded")
|
||||
|
||||
if not isinstance(item, MediaGalleryItem):
|
||||
raise TypeError(f"expected MediaGalleryItem not {item.__class__!r}")
|
||||
|
||||
self.underlying.items.append(item)
|
||||
return self
|
||||
|
||||
def add_item(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
description: str = None,
|
||||
spoiler: bool = False,
|
||||
) -> Self:
|
||||
"""Adds a new media item to the gallery.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url: :class:`str`
|
||||
The URL of the media item. This can either be an arbitrary URL or an ``attachment://`` URL.
|
||||
description: Optional[:class:`str`]
|
||||
The media item's description, up to 1024 characters.
|
||||
spoiler: Optional[:class:`bool`]
|
||||
Whether the media item has the spoiler overlay.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
Maximum number of items has been exceeded (10).
|
||||
"""
|
||||
|
||||
if len(self.items) >= 10:
|
||||
raise ValueError("maximum number of items exceeded")
|
||||
|
||||
item = MediaGalleryItem(url, description=description, spoiler=spoiler)
|
||||
|
||||
return self.append_item(item)
|
||||
|
||||
def remove_item(self, index: int) -> Self:
|
||||
"""Removes an item from the gallery.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
index: :class:`int`
|
||||
The index of the item to remove from the gallery.
|
||||
"""
|
||||
|
||||
try:
|
||||
self.items.pop(index)
|
||||
except IndexError:
|
||||
pass
|
||||
return self
|
||||
|
||||
def to_component_dict(self) -> MediaGalleryComponentPayload:
|
||||
self._underlying = self._generate_underlying()
|
||||
return super().to_component_dict()
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[M], component: MediaGalleryComponent) -> M:
|
||||
return cls(*component.items, id=component.id)
|
||||
|
||||
callback = None
|
||||
@@ -0,0 +1,527 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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 os
|
||||
import sys
|
||||
import time
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
from typing import TYPE_CHECKING, Any, Iterator, TypeVar
|
||||
|
||||
from ..enums import ComponentType
|
||||
from ..utils import _get_event_loop, find
|
||||
from .core import ItemInterface
|
||||
from .input_text import InputText
|
||||
from .item import ModalItem
|
||||
from .label import Label
|
||||
from .select import Select
|
||||
from .text_display import TextDisplay
|
||||
|
||||
__all__ = (
|
||||
"BaseModal",
|
||||
"Modal",
|
||||
"DesignerModal",
|
||||
"ModalStore",
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..interactions import Interaction
|
||||
from ..state import ConnectionState
|
||||
from ..types.components import Component as ComponentPayload
|
||||
|
||||
M = TypeVar("M", bound="Modal", covariant=True)
|
||||
|
||||
|
||||
class BaseModal(ItemInterface):
|
||||
"""The base class for creating pop-up modals.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"title",
|
||||
"children",
|
||||
"timeout",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: ModalItem,
|
||||
title: str,
|
||||
custom_id: str | None = None,
|
||||
timeout: float | None = None,
|
||||
store: bool = True,
|
||||
) -> None:
|
||||
if not isinstance(custom_id, str) and custom_id is not None:
|
||||
raise TypeError(
|
||||
f"expected custom_id to be str, not {custom_id.__class__.__name__}"
|
||||
)
|
||||
self._custom_id: str | None = custom_id or os.urandom(16).hex()
|
||||
if len(title) > 45:
|
||||
raise ValueError("title must be 45 characters or fewer")
|
||||
self._children: list[ModalItem] = []
|
||||
super().__init__(timeout=timeout, store=store)
|
||||
for item in children:
|
||||
self.add_item(item)
|
||||
self._title = title
|
||||
self.loop = _get_event_loop()
|
||||
|
||||
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}>"
|
||||
|
||||
def _start_listening_from_store(self, store: ModalStore) -> None:
|
||||
self._cancel_callback = partial(store.remove_modal)
|
||||
if self.timeout:
|
||||
loop = asyncio.get_running_loop()
|
||||
if self._timeout_task is not None:
|
||||
self._timeout_task.cancel()
|
||||
|
||||
self._timeout_expiry = time.monotonic() + self.timeout
|
||||
self._timeout_task = loop.create_task(self._timeout_task_impl())
|
||||
|
||||
def _dispatch_timeout(self):
|
||||
if self._stopped.done():
|
||||
return
|
||||
|
||||
self._stopped.set_result(True)
|
||||
self.loop.create_task(
|
||||
self.on_timeout(), name=f"discord-ui-view-timeout-{self.custom_id}"
|
||||
)
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
"""The title of the modal."""
|
||||
return self._title
|
||||
|
||||
@title.setter
|
||||
def title(self, value: str):
|
||||
if len(value) > 45:
|
||||
raise ValueError("title must be 45 characters or fewer")
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"expected title to be str, not {value.__class__.__name__}")
|
||||
self._title = value
|
||||
|
||||
@property
|
||||
def children(self) -> list[ModalItem]:
|
||||
"""The child items attached to the modal."""
|
||||
return self._children
|
||||
|
||||
@children.setter
|
||||
def children(self, value: list[ModalItem]):
|
||||
for item in value:
|
||||
if not isinstance(item, ModalItem):
|
||||
raise TypeError(
|
||||
"all BaseModal children must be ModalItem, not"
|
||||
f" {item.__class__.__name__}"
|
||||
)
|
||||
self._children = value
|
||||
|
||||
@property
|
||||
def custom_id(self) -> str:
|
||||
"""The ID of the modal that gets received during an interaction."""
|
||||
return self._custom_id
|
||||
|
||||
@custom_id.setter
|
||||
def custom_id(self, value: str):
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(
|
||||
f"expected custom_id to be str, not {value.__class__.__name__}"
|
||||
)
|
||||
if len(value) > 100:
|
||||
raise ValueError("custom_id must be 100 characters or fewer")
|
||||
self._custom_id = value
|
||||
|
||||
async def callback(self, interaction: Interaction):
|
||||
"""|coro|
|
||||
|
||||
The coroutine that is called when the modal is submitted.
|
||||
Should be overridden to handle the values submitted by the user.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction: :class:`~discord.Interaction`
|
||||
The interaction that submitted the modal.
|
||||
"""
|
||||
self.stop()
|
||||
|
||||
def add_item(self, item: ModalItem) -> Self:
|
||||
"""Adds a component to the modal.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: Union[:class:`ModalItem`]
|
||||
The item to add to the modal
|
||||
"""
|
||||
|
||||
if len(self._children) >= 5:
|
||||
raise ValueError("You can only have up to 5 items in a modal.")
|
||||
|
||||
if not isinstance(item, ModalItem):
|
||||
raise TypeError(f"expected ModalItem, not {item.__class__!r}")
|
||||
|
||||
self._children.append(item)
|
||||
return self
|
||||
|
||||
def remove_item(self, item: ModalItem) -> Self:
|
||||
"""Removes a component from the modal.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`ModalItem`
|
||||
The item to remove from the modal.
|
||||
"""
|
||||
try:
|
||||
self._children.remove(item)
|
||||
except ValueError:
|
||||
pass
|
||||
return self
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stops listening to interaction events from the modal."""
|
||||
if not self._stopped.done():
|
||||
self._stopped.set_result(True)
|
||||
self._timeout_expiry = None
|
||||
if self._timeout_task is not None:
|
||||
self._timeout_task.cancel()
|
||||
self._timeout_task = None
|
||||
|
||||
async def wait(self) -> bool:
|
||||
"""Waits for the modal to be submitted."""
|
||||
return await self._stopped
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"title": self.title,
|
||||
"custom_id": self.custom_id,
|
||||
"components": self.to_components(),
|
||||
}
|
||||
|
||||
async def on_error(self, error: Exception, interaction: Interaction) -> None:
|
||||
"""|coro|
|
||||
|
||||
A callback that is called when the modal's callback fails with an error.
|
||||
|
||||
The default implementation prints the traceback to stderr.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
error: :class:`Exception`
|
||||
The exception that was raised.
|
||||
interaction: :class:`~discord.Interaction`
|
||||
The interaction that led to the failure.
|
||||
"""
|
||||
interaction.client.dispatch("modal_error", error, interaction)
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
A callback that is called when a modal's timeout elapses without being explicitly stopped.
|
||||
"""
|
||||
|
||||
def walk_children(self) -> Iterator[ModalItem]:
|
||||
for item in self.children:
|
||||
if hasattr(item, "walk_items"):
|
||||
yield from item.walk_items()
|
||||
else:
|
||||
yield item
|
||||
|
||||
|
||||
class Modal(BaseModal):
|
||||
"""Represents a legacy UI modal for InputText components.
|
||||
|
||||
This object must be inherited to create a UI within Discord.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
|
||||
Now inherits from :class:`BaseModal`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
children: Union[:class:`InputText`]
|
||||
The initial items that are displayed in the modal. Only supports :class:`discord.ui.InputText`; for newer modal features, see :class:`DesignerModal`.
|
||||
title: :class:`str`
|
||||
The title of the modal.
|
||||
Must be 45 characters or fewer.
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the modal that gets received during an interaction.
|
||||
Must be 100 characters or fewer.
|
||||
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.
|
||||
store: Optional[:class:`bool`]
|
||||
Whether this modal should be stored for callback listening. Setting it to ``False`` will ignore its callback and prevent item values from being refreshed. Defaults to ``True``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: InputText,
|
||||
title: str,
|
||||
custom_id: str | None = None,
|
||||
timeout: float | None = None,
|
||||
store: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children, title=title, custom_id=custom_id, timeout=timeout, store=store
|
||||
)
|
||||
self._weights = _ModalWeights(self._children)
|
||||
|
||||
@property
|
||||
def children(self) -> list[InputText]:
|
||||
return self._children
|
||||
|
||||
@children.setter
|
||||
def children(self, value: list[InputText]):
|
||||
for item in value:
|
||||
if not isinstance(item, InputText):
|
||||
raise TypeError(
|
||||
"all Modal children must be InputText, not"
|
||||
f" {item.__class__.__name__}"
|
||||
)
|
||||
self._weights = _ModalWeights(self._children)
|
||||
self._children = value
|
||||
|
||||
def to_components(self) -> list[dict[str, Any]]:
|
||||
def key(item: InputText) -> int:
|
||||
return item._rendered_row or 0
|
||||
|
||||
children = sorted(self._children, key=key)
|
||||
components: list[dict[str, Any]] = []
|
||||
for _, group in groupby(children, key=key):
|
||||
children = [item.to_component_dict() for item in group]
|
||||
if not children:
|
||||
continue
|
||||
|
||||
components.append(
|
||||
{
|
||||
"type": 1,
|
||||
"components": children,
|
||||
}
|
||||
)
|
||||
|
||||
return components
|
||||
|
||||
def add_item(self, item: InputText) -> Self:
|
||||
"""Adds an InputText component to the modal.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`InputText`
|
||||
The item to add to the modal
|
||||
"""
|
||||
|
||||
if not isinstance(item, InputText):
|
||||
raise TypeError(f"expected InputText not {item.__class__!r}")
|
||||
|
||||
self._weights.add_item(item)
|
||||
super().add_item(item)
|
||||
return self
|
||||
|
||||
def remove_item(self, item: InputText) -> Self:
|
||||
"""Removes an InputText from the modal.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: Union[class:`InputText`]
|
||||
The item to remove from the modal.
|
||||
"""
|
||||
|
||||
super().remove_item(item)
|
||||
try:
|
||||
self.__weights.remove_item(item)
|
||||
except ValueError:
|
||||
pass
|
||||
return self
|
||||
|
||||
def _refresh(self, interaction: Interaction, data: list[ComponentPayload]):
|
||||
components = [
|
||||
component
|
||||
for parent_component in data
|
||||
for component in parent_component["components"]
|
||||
]
|
||||
for component in components:
|
||||
for child in self.children:
|
||||
if child.custom_id == component["custom_id"]: # type: ignore
|
||||
child.refresh_from_modal(interaction, component)
|
||||
break
|
||||
|
||||
|
||||
class DesignerModal(BaseModal):
|
||||
"""Represents a UI modal compatible with all modal features.
|
||||
|
||||
This object must be inherited to create a UI within Discord.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
children: Union[:class:`ModalItem`]
|
||||
The initial items that are displayed in the modal..
|
||||
title: :class:`str`
|
||||
The title of the modal.
|
||||
Must be 45 characters or fewer.
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the modal that gets received during an interaction.
|
||||
Must be 100 characters or fewer.
|
||||
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.
|
||||
store: Optional[:class:`bool`]
|
||||
Whether this modal should be stored for callback listening. Setting it to ``False`` will ignore its callback and prevent item values from being refreshed. Defaults to ``True``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: ModalItem,
|
||||
title: str,
|
||||
custom_id: str | None = None,
|
||||
timeout: float | None = None,
|
||||
store: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children, title=title, custom_id=custom_id, timeout=timeout, store=store
|
||||
)
|
||||
|
||||
@property
|
||||
def children(self) -> list[ModalItem]:
|
||||
return self._children
|
||||
|
||||
@children.setter
|
||||
def children(self, value: list[ModalItem]):
|
||||
for item in value:
|
||||
if not isinstance(item, ModalItem):
|
||||
raise TypeError(
|
||||
"all DesignerModal children must be ModalItem, not"
|
||||
f" {item.__class__.__name__}"
|
||||
)
|
||||
if isinstance(item, (InputText,)):
|
||||
raise TypeError(
|
||||
f"DesignerModal does not accept InputText directly. Use Label instead."
|
||||
)
|
||||
self._children = value
|
||||
|
||||
def add_item(self, item: ModalItem) -> Self:
|
||||
"""Adds a component to the modal.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: Union[:class:`ModalItem`]
|
||||
The item to add to the modal
|
||||
"""
|
||||
|
||||
if isinstance(item, (InputText,)):
|
||||
raise TypeError(
|
||||
f"DesignerModal does not accept InputText directly. Use Label instead."
|
||||
)
|
||||
|
||||
super().add_item(item)
|
||||
return self
|
||||
|
||||
def _refresh(self, interaction: Interaction, data: list[ComponentPayload]):
|
||||
for component, child in zip(data, self.children):
|
||||
child.refresh_from_modal(interaction, component)
|
||||
|
||||
|
||||
class _ModalWeights:
|
||||
__slots__ = ("weights",)
|
||||
|
||||
def __init__(self, children: list[InputText]):
|
||||
self.weights: list[int] = [0, 0, 0, 0, 0]
|
||||
|
||||
key = lambda i: sys.maxsize if i.row is None else i.row
|
||||
children = sorted(children, key=key)
|
||||
for row, group in groupby(children, key=key):
|
||||
for item in group:
|
||||
self.add_item(item)
|
||||
|
||||
def find_open_space(self, item: InputText) -> int:
|
||||
for index, weight in enumerate(self.weights):
|
||||
if weight + item.width <= 5:
|
||||
return index
|
||||
|
||||
raise ValueError("could not find open space for item")
|
||||
|
||||
def add_item(self, item: InputText) -> None:
|
||||
if item.row is not None:
|
||||
total = self.weights[item.row] + item.width
|
||||
if total > 5:
|
||||
raise ValueError(
|
||||
f"item would not fit at row {item.row} ({total} > 5 width)"
|
||||
)
|
||||
self.weights[item.row] = total
|
||||
item._rendered_row = item.row
|
||||
else:
|
||||
index = self.find_open_space(item)
|
||||
self.weights[index] += item.width
|
||||
item._rendered_row = index
|
||||
|
||||
def remove_item(self, item: InputText) -> None:
|
||||
if item._rendered_row is not None:
|
||||
self.weights[item._rendered_row] -= item.width
|
||||
item._rendered_row = None
|
||||
|
||||
def clear(self) -> None:
|
||||
self.weights = [0, 0, 0, 0, 0]
|
||||
|
||||
|
||||
class ModalStore:
|
||||
def __init__(self, state: ConnectionState) -> None:
|
||||
# (user_id, custom_id) : Modal
|
||||
self._modals: dict[tuple[int, str], BaseModal] = {}
|
||||
self._state: ConnectionState = state
|
||||
|
||||
def add_modal(self, modal: BaseModal, user_id: int):
|
||||
if not modal._store:
|
||||
return
|
||||
self._modals[(user_id, modal.custom_id)] = modal
|
||||
modal._start_listening_from_store(self)
|
||||
|
||||
def remove_modal(self, modal: BaseModal, user_id):
|
||||
modal.stop()
|
||||
self._modals.pop((user_id, modal.custom_id))
|
||||
|
||||
async def dispatch(self, user_id: int, custom_id: str, interaction: Interaction):
|
||||
key = (user_id, custom_id)
|
||||
modal = self._modals.get(key)
|
||||
if modal is None:
|
||||
return
|
||||
interaction.modal = modal
|
||||
|
||||
try:
|
||||
components = interaction.data["components"]
|
||||
modal._refresh(interaction, components)
|
||||
await modal.callback(interaction)
|
||||
self.remove_modal(modal, user_id)
|
||||
except Exception as e:
|
||||
return await modal.on_error(e, interaction)
|
||||
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..components import RadioGroup as RadioGroupComponent
|
||||
from ..components import RadioGroupOption
|
||||
from ..enums import ComponentType
|
||||
from ..utils import MISSING
|
||||
from .item import ModalItem
|
||||
|
||||
__all__ = ("RadioGroup",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from ..types.components import RadioGroupComponent as RadioGroupComponentPayload
|
||||
|
||||
|
||||
class RadioGroup(ModalItem):
|
||||
"""Represents a UI Radio Group component.
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
Attributes
|
||||
----------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the radio group that gets received during an interaction.
|
||||
options: List[:class:`discord.RadioGroupOption`]
|
||||
A list of options that can be selected from this group. Must provide between 2 and 10 options.
|
||||
required: Optional[:class:`bool`]
|
||||
Whether an option selection is required or not. Defaults to ``True``.
|
||||
id: Optional[:class:`int`]
|
||||
The radio group's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"options",
|
||||
"required",
|
||||
"custom_id",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
custom_id: str | None = None,
|
||||
options: list[RadioGroupOption] | None = None,
|
||||
required: bool = True,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
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 not isinstance(required, bool):
|
||||
raise TypeError(f"required must be bool not {required.__class__.__name__}")
|
||||
custom_id = os.urandom(16).hex() if custom_id is None else custom_id
|
||||
self._selected_value: str | None = None
|
||||
|
||||
self._underlying: RadioGroupComponent = self._generate_underlying(
|
||||
custom_id=custom_id,
|
||||
options=[],
|
||||
required=required,
|
||||
id=id,
|
||||
)
|
||||
if options:
|
||||
self.options = options
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
custom_id: str | None = None,
|
||||
options: list[RadioGroupOption] | None = None,
|
||||
required: bool | None = None,
|
||||
id: int | None = None,
|
||||
) -> RadioGroupComponent:
|
||||
super()._generate_underlying(RadioGroupComponent)
|
||||
return RadioGroupComponent._raw_construct(
|
||||
type=ComponentType.radio_group,
|
||||
custom_id=custom_id or self.custom_id,
|
||||
options=options if options is not None else self.options,
|
||||
required=required if required is not None else self.required,
|
||||
id=id or self.id,
|
||||
)
|
||||
|
||||
@property
|
||||
def custom_id(self) -> str:
|
||||
"""The custom id that gets received during an interaction."""
|
||||
return self.underlying.custom_id
|
||||
|
||||
@custom_id.setter
|
||||
def custom_id(self, value: str):
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"custom_id must be str not {value.__class__.__name__}")
|
||||
if len(value) > 100:
|
||||
raise ValueError("custom_id must be 100 characters or fewer")
|
||||
self.underlying.custom_id = value
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
"""Whether an option selection is required or not. Defaults to ``True``"""
|
||||
return self.underlying.required
|
||||
|
||||
@required.setter
|
||||
def required(self, value: bool):
|
||||
if not isinstance(value, bool):
|
||||
raise TypeError(f"required must be bool, not {value.__class__.__name__}")
|
||||
self.underlying.required = bool(value)
|
||||
|
||||
@property
|
||||
def value(self) -> str | None:
|
||||
"""The value selected by the user. May return ``None`` if this radio group is optional or has not been sent yet."""
|
||||
return self._selected_value
|
||||
|
||||
@property
|
||||
def options(self) -> list[RadioGroupOption]:
|
||||
"""A list of options that can be selected in this group."""
|
||||
return self.underlying.options
|
||||
|
||||
@options.setter
|
||||
def options(self, value: list[RadioGroupOption]):
|
||||
if not isinstance(value, list):
|
||||
raise TypeError("options must be a list of RadioGroupOption")
|
||||
if not (2 <= len(value) <= 10) and len(value) != 0:
|
||||
raise ValueError("you must provide between 2 and 10 options.")
|
||||
if not all(isinstance(obj, RadioGroupOption) for obj in value):
|
||||
raise TypeError("all list items must subclass RadioGroupOption")
|
||||
if len([o for o in value if o.default]) > 1:
|
||||
raise ValueError("only 1 option can be set as default.")
|
||||
|
||||
self.underlying.options = value
|
||||
|
||||
def add_option(
|
||||
self,
|
||||
*,
|
||||
label: str,
|
||||
value: str = MISSING,
|
||||
description: str | None = None,
|
||||
default: bool = False,
|
||||
) -> Self:
|
||||
"""Adds an option to the radio group.
|
||||
|
||||
To append a pre-existing :class:`discord.RadioGroupOption` use the
|
||||
:meth:`append_option` method instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label: :class:`str`
|
||||
The label of the option. This is displayed to users.
|
||||
Can only be up to 100 characters.
|
||||
value: :class:`str`
|
||||
The value of the option. This is not displayed to users.
|
||||
If not given, defaults to the label. Can only be up to 100 characters.
|
||||
description: Optional[:class:`str`]
|
||||
An additional description of the option, if any.
|
||||
Can only be up to 100 characters.
|
||||
default: :class:`bool`
|
||||
Whether this option is selected by default.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
The number of options exceeds 10.
|
||||
"""
|
||||
|
||||
option = RadioGroupOption(
|
||||
label=label,
|
||||
value=value,
|
||||
description=description,
|
||||
default=default,
|
||||
)
|
||||
|
||||
return self.append_option(option)
|
||||
|
||||
def append_option(self, option: RadioGroupOption) -> Self:
|
||||
"""Appends an option to the radio group.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
option: :class:`discord.RadioGroupOption`
|
||||
The option to append to the radio group.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
The number of options exceeds 10.
|
||||
"""
|
||||
|
||||
if len(self.underlying.options) >= 10:
|
||||
raise ValueError("maximum number of options already provided")
|
||||
|
||||
self.underlying.options.append(option)
|
||||
return self
|
||||
|
||||
def clear_options(self) -> Self:
|
||||
"""Remove all options from the radio group."""
|
||||
|
||||
self.underlying.options.clear()
|
||||
return self
|
||||
|
||||
def to_component_dict(self) -> RadioGroupComponentPayload:
|
||||
return self.underlying.to_dict()
|
||||
|
||||
def refresh_state(self, data) -> None:
|
||||
self._selected_value = data.get("value", None)
|
||||
|
||||
def refresh_from_modal(
|
||||
self, interaction: Interaction, data: RadioGroupComponentPayload
|
||||
) -> None:
|
||||
return self.refresh_state(data)
|
||||
@@ -0,0 +1,358 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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 functools import partial
|
||||
from typing import TYPE_CHECKING, ClassVar, Iterator, TypeVar
|
||||
|
||||
from ..components import Section as SectionComponent
|
||||
from ..components import _component_factory
|
||||
from ..enums import ComponentType
|
||||
from ..utils import find, get
|
||||
from .button import Button
|
||||
from .item import ItemCallbackType, ViewItem
|
||||
from .text_display import TextDisplay
|
||||
from .thumbnail import Thumbnail
|
||||
|
||||
__all__ = ("Section",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..types.components import SectionComponent as SectionComponentPayload
|
||||
from .view import DesignerView
|
||||
|
||||
|
||||
S = TypeVar("S", bound="Section")
|
||||
V = TypeVar("V", bound="DesignerView", covariant=True)
|
||||
|
||||
|
||||
class Section(ViewItem[V]):
|
||||
"""Represents a UI section. Sections must have 1-3 (inclusive) items and an accessory set.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*items: :class:`ViewItem`
|
||||
The initial items contained in this section, up to 3.
|
||||
Currently only supports :class:`~discord.ui.TextDisplay`.
|
||||
Sections must have at least 1 item before being sent.
|
||||
accessory: Optional[:class:`ViewItem`]
|
||||
The section's accessory. This is displayed in the top right of the section.
|
||||
Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`.
|
||||
Sections must have an accessory attached before being sent.
|
||||
id: Optional[:class:`int`]
|
||||
The section's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
items: List[:class:`ViewItem`]
|
||||
The list of items in this section.
|
||||
accessory: :class:`ViewItem`
|
||||
The section's accessory, displayed in the top right of the section.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"items",
|
||||
"accessory",
|
||||
"id",
|
||||
)
|
||||
|
||||
__section_accessory_item__: ClassVar[ItemCallbackType] = []
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
accessory: list[ItemCallbackType] = []
|
||||
for base in reversed(cls.__mro__):
|
||||
for member in base.__dict__.values():
|
||||
if hasattr(member, "__discord_ui_model_type__"):
|
||||
accessory.append(member)
|
||||
|
||||
cls.__section_accessory_item__ = accessory
|
||||
|
||||
def __init__(
|
||||
self, *items: ViewItem, accessory: ViewItem = None, id: int | None = None
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.items: list[ViewItem] = []
|
||||
self.accessory: ViewItem | None = None
|
||||
|
||||
self._underlying = self._generate_underlying(
|
||||
id=id,
|
||||
)
|
||||
for func in self.__section_accessory_item__:
|
||||
item: ViewItem = func.__discord_ui_model_type__(
|
||||
**func.__discord_ui_model_kwargs__
|
||||
)
|
||||
item.callback = partial(func, self, item)
|
||||
self.set_accessory(item)
|
||||
setattr(self, func.__name__, item)
|
||||
if accessory:
|
||||
self.set_accessory(accessory)
|
||||
for i in items:
|
||||
self.add_item(i)
|
||||
|
||||
def _add_component_from_item(self, item: ViewItem):
|
||||
self.underlying.components.append(item.underlying)
|
||||
|
||||
def _set_components(self, items: list[ViewItem]):
|
||||
self.underlying.components.clear()
|
||||
for item in items:
|
||||
self._add_component_from_item(item)
|
||||
|
||||
def _generate_underlying(self, id: int | None = None) -> SectionComponent:
|
||||
super()._generate_underlying(SectionComponent)
|
||||
section = SectionComponent._raw_construct(
|
||||
type=ComponentType.section,
|
||||
id=id or self.id,
|
||||
components=[],
|
||||
accessory=None,
|
||||
)
|
||||
for i in self.items:
|
||||
section.components.append(i._generate_underlying())
|
||||
if self.accessory:
|
||||
section.accessory = self.accessory._generate_underlying()
|
||||
return section
|
||||
|
||||
def add_item(self, item: ViewItem) -> Self:
|
||||
"""Adds an item to the section.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`ViewItem`
|
||||
The item to add to the section.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
An :class:`ViewItem` was not passed.
|
||||
ValueError
|
||||
Maximum number of items has been exceeded (3).
|
||||
"""
|
||||
|
||||
if len(self.items) >= 3:
|
||||
raise ValueError("maximum number of children exceeded")
|
||||
|
||||
if not isinstance(item, ViewItem):
|
||||
raise TypeError(f"expected ViewItem not {item.__class__!r}")
|
||||
|
||||
item.parent = self
|
||||
self.items.append(item)
|
||||
self._add_component_from_item(item)
|
||||
return self
|
||||
|
||||
def remove_item(self, item: ViewItem | str | int) -> Self:
|
||||
"""Removes an item from the section. If an :class:`int` or :class:`str` is passed,
|
||||
the item will be removed by Item ``id`` or ``custom_id`` respectively.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: Union[:class:`ViewItem`, :class:`int`, :class:`str`]
|
||||
The item, item ``id``, or item ``custom_id`` to remove from the section.
|
||||
"""
|
||||
|
||||
if isinstance(item, (str, int)):
|
||||
item = self.get_item(item)
|
||||
try:
|
||||
if item is self.accessory:
|
||||
self.accessory = None
|
||||
else:
|
||||
self.items.remove(item)
|
||||
item.parent = None
|
||||
except ValueError:
|
||||
pass
|
||||
return self
|
||||
|
||||
def get_item(self, id: int | str) -> ViewItem | None:
|
||||
"""Get an item from this section. Alias for `utils.get(section.walk_items(), ...)`.
|
||||
If an ``int`` is provided, it will be retrieved by ``id``, otherwise it will check the accessory's ``custom_id``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id: Union[:class:`str`, :class:`int`]
|
||||
The id or custom_id of the item to get.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`ViewItem`]
|
||||
The item with the matching ``id`` if it exists.
|
||||
"""
|
||||
if not id:
|
||||
return None
|
||||
attr = "id" if isinstance(id, int) else "custom_id"
|
||||
if self.accessory and id == getattr(self.accessory, attr, None):
|
||||
return self.accessory
|
||||
child = find(lambda i: getattr(i, attr, None) == id, self.items)
|
||||
return child
|
||||
|
||||
def add_text(self, content: str, *, id: int | None = None) -> Self:
|
||||
"""Adds a :class:`TextDisplay` to the section.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
content: :class:`str`
|
||||
The content of the text display.
|
||||
id: Optional[:class:`int`]
|
||||
The text display's ID.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
Maximum number of items has been exceeded (3).
|
||||
"""
|
||||
|
||||
if len(self.items) >= 3:
|
||||
raise ValueError("maximum number of children exceeded")
|
||||
|
||||
text = TextDisplay(content, id=id)
|
||||
|
||||
return self.add_item(text)
|
||||
|
||||
def set_accessory(self, item: ViewItem) -> Self:
|
||||
"""Set an item as the section's :attr:`accessory`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`ViewItem`
|
||||
The item to set as accessory.
|
||||
Currently only supports :class:`~discord.ui.Button` and :class:`~discord.ui.Thumbnail`.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
An :class:`ViewItem` was not passed.
|
||||
"""
|
||||
|
||||
if not isinstance(item, ViewItem):
|
||||
raise TypeError(f"expected ViewItem not {item.__class__!r}")
|
||||
|
||||
item.parent = self
|
||||
|
||||
self.accessory = item
|
||||
self.underlying.accessory = item._generate_underlying()
|
||||
return self
|
||||
|
||||
def set_thumbnail(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
description: str | None = None,
|
||||
spoiler: bool = False,
|
||||
id: int | None = None,
|
||||
) -> Self:
|
||||
"""Sets a :class:`Thumbnail` with the provided URL as the section's :attr:`accessory`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url: :class:`str`
|
||||
The url of the thumbnail.
|
||||
description: Optional[:class:`str`]
|
||||
The thumbnail's description, up to 1024 characters.
|
||||
spoiler: Optional[:class:`bool`]
|
||||
Whether the thumbnail has the spoiler overlay. Defaults to ``False``.
|
||||
id: Optional[:class:`int`]
|
||||
The thumbnail's ID.
|
||||
"""
|
||||
|
||||
thumbnail = Thumbnail(url, description=description, spoiler=spoiler, id=id)
|
||||
|
||||
return self.set_accessory(thumbnail)
|
||||
|
||||
def copy_text(self) -> str:
|
||||
"""Returns the text of all :class:`~discord.ui.TextDisplay` items in this section.
|
||||
Equivalent to the `Copy Text` option on Discord clients.
|
||||
"""
|
||||
return "\n".join(t for i in self.items if (t := i.copy_text()))
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return self.accessory and self.accessory.is_dispatchable()
|
||||
|
||||
def is_persistent(self) -> bool:
|
||||
if not isinstance(self.accessory, Button):
|
||||
return True
|
||||
return self.accessory.is_persistent()
|
||||
|
||||
def refresh_component(self, component: SectionComponent) -> None:
|
||||
self.underlying = component
|
||||
for x, y in zip(self.items, component.components):
|
||||
x.refresh_component(y)
|
||||
if self.accessory and component.accessory:
|
||||
self.accessory.refresh_component(component.accessory)
|
||||
|
||||
def disable_all_items(self, *, exclusions: list[ViewItem] | None = None) -> Self:
|
||||
"""
|
||||
Disables all buttons and select menus in the section.
|
||||
At the moment, this only disables :attr:`accessory` if it is a button.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exclusions: Optional[List[:class:`ViewItem`]]
|
||||
A list of items in `self.items` to not disable from the view.
|
||||
"""
|
||||
for item in self.walk_items():
|
||||
if hasattr(item, "disabled") and (
|
||||
exclusions is None or item not in exclusions
|
||||
):
|
||||
item.disabled = True
|
||||
return self
|
||||
|
||||
def enable_all_items(self, *, exclusions: list[ViewItem] | None = None) -> Self:
|
||||
"""
|
||||
Enables all buttons and select menus in the section.
|
||||
At the moment, this only enables :attr:`accessory` if it is a button.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exclusions: Optional[List[:class:`ViewItem`]]
|
||||
A list of items in `self.items` to not enable from the view.
|
||||
"""
|
||||
for item in self.walk_items():
|
||||
if hasattr(item, "disabled") and (
|
||||
exclusions is None or item not in exclusions
|
||||
):
|
||||
item.disabled = False
|
||||
return self
|
||||
|
||||
def walk_items(self) -> Iterator[ViewItem]:
|
||||
r = self.items
|
||||
if self.accessory:
|
||||
yield from r + [self.accessory]
|
||||
else:
|
||||
yield from r
|
||||
|
||||
def to_component_dict(self) -> SectionComponentPayload:
|
||||
self._underlying = self._generate_underlying()
|
||||
return super().to_component_dict()
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[S], component: SectionComponent) -> S:
|
||||
from .view import _component_to_item
|
||||
|
||||
items = [_component_to_item(c) for c in component.components]
|
||||
accessory = _component_to_item(component.accessory)
|
||||
return cls(*items, accessory=accessory, id=component.id)
|
||||
|
||||
callback = None
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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, TypeVar
|
||||
|
||||
from ..components import Separator as SeparatorComponent
|
||||
from ..components import _component_factory
|
||||
from ..enums import ComponentType, SeparatorSpacingSize
|
||||
from .item import ViewItem
|
||||
|
||||
__all__ = ("Separator",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..types.components import SeparatorComponent as SeparatorComponentPayload
|
||||
from .view import DesignerView
|
||||
|
||||
|
||||
S = TypeVar("S", bound="Separator")
|
||||
V = TypeVar("V", bound="DesignerView", covariant=True)
|
||||
|
||||
|
||||
class Separator(ViewItem[V]):
|
||||
"""Represents a UI Separator.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
divider: :class:`bool`
|
||||
Whether the separator is a divider. Defaults to ``True``.
|
||||
spacing: :class:`~discord.SeparatorSpacingSize`
|
||||
The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`.
|
||||
id: Optional[:class:`int`]
|
||||
The separator's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"divider",
|
||||
"spacing",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
divider: bool = True,
|
||||
spacing: SeparatorSpacingSize = SeparatorSpacingSize.small,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self._underlying = self._generate_underlying(
|
||||
id=id,
|
||||
divider=divider,
|
||||
spacing=spacing,
|
||||
)
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
divider: bool | None = None,
|
||||
spacing: SeparatorSpacingSize | None = None,
|
||||
id: int | None = None,
|
||||
) -> SeparatorComponent:
|
||||
super()._generate_underlying(SeparatorComponent)
|
||||
return SeparatorComponent._raw_construct(
|
||||
type=ComponentType.separator,
|
||||
id=id or self.id,
|
||||
divider=divider if divider is not None else self.divider,
|
||||
spacing=spacing or self.spacing,
|
||||
)
|
||||
|
||||
@property
|
||||
def divider(self) -> bool:
|
||||
"""Whether the separator is a divider. Defaults to ``True``."""
|
||||
return self.underlying.divider
|
||||
|
||||
@divider.setter
|
||||
def divider(self, value: bool) -> None:
|
||||
self.underlying.divider = value
|
||||
|
||||
@property
|
||||
def spacing(self) -> SeparatorSpacingSize:
|
||||
"""The spacing size of the separator. Defaults to :attr:`~discord.SeparatorSpacingSize.small`."""
|
||||
return self.underlying.spacing
|
||||
|
||||
@spacing.setter
|
||||
def spacing(self, value: SeparatorSpacingSize) -> None:
|
||||
self.underlying.spacing = value
|
||||
|
||||
def to_component_dict(self) -> SeparatorComponentPayload:
|
||||
return super().to_component_dict()
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[S], component: SeparatorComponent) -> S:
|
||||
return cls(
|
||||
divider=component.divider, spacing=component.spacing, id=component.id
|
||||
)
|
||||
|
||||
callback = None
|
||||
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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, TypeVar
|
||||
|
||||
from ..components import TextDisplay as TextDisplayComponent
|
||||
from ..components import _component_factory
|
||||
from ..enums import ComponentType
|
||||
from .item import ModalItem, ViewItem
|
||||
|
||||
__all__ = ("TextDisplay",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..types.components import TextDisplayComponent as TextDisplayComponentPayload
|
||||
from .core import ItemInterface
|
||||
from .modal import DesignerModal
|
||||
from .view import DesignerView
|
||||
|
||||
|
||||
T = TypeVar("T", bound="TextDisplay")
|
||||
V = TypeVar("V", bound="DesignerView", covariant=True)
|
||||
M = TypeVar("M", bound="DesignerModal", covariant=True)
|
||||
|
||||
|
||||
class TextDisplay(ViewItem[V], ModalItem[M]):
|
||||
"""Represents a UI text display. A message can have up to 4000 characters across all :class:`TextDisplay` objects combined.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
content: :class:`str`
|
||||
The text display's content, up to 4000 characters.
|
||||
id: Optional[:class:`int`]
|
||||
The text display's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"content",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: str,
|
||||
id: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self._underlying = self._generate_underlying(
|
||||
id=id,
|
||||
content=content,
|
||||
)
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
content: str | None = None,
|
||||
id: int | None = None,
|
||||
) -> TextDisplayComponent:
|
||||
super()._generate_underlying(TextDisplayComponent)
|
||||
return TextDisplayComponent._raw_construct(
|
||||
type=ComponentType.text_display,
|
||||
id=id or self.id,
|
||||
content=content or self.content,
|
||||
)
|
||||
|
||||
@property
|
||||
def content(self) -> str:
|
||||
"""The text display's content."""
|
||||
return self.underlying.content
|
||||
|
||||
@content.setter
|
||||
def content(self, value: str) -> None:
|
||||
self.underlying.content = value
|
||||
|
||||
def to_component_dict(self) -> TextDisplayComponentPayload:
|
||||
return super().to_component_dict()
|
||||
|
||||
def copy_text(self) -> str:
|
||||
"""Returns the content of this text display. Equivalent to the `Copy Text` option on Discord clients."""
|
||||
return self.content
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[T], component: TextDisplayComponent) -> T:
|
||||
return cls(component.content, id=component.id)
|
||||
|
||||
callback = None
|
||||
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-present Pycord Development
|
||||
|
||||
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, TypeVar
|
||||
|
||||
from ..components import Thumbnail as ThumbnailComponent
|
||||
from ..components import UnfurledMediaItem, _component_factory
|
||||
from ..enums import ComponentType
|
||||
from .item import ViewItem
|
||||
|
||||
__all__ = ("Thumbnail",)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..types.components import ThumbnailComponent as ThumbnailComponentPayload
|
||||
from .view import DesignerView
|
||||
|
||||
|
||||
T = TypeVar("T", bound="Thumbnail")
|
||||
V = TypeVar("V", bound="DesignerView", covariant=True)
|
||||
|
||||
|
||||
class Thumbnail(ViewItem[V]):
|
||||
"""Represents a UI Thumbnail.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url: :class:`str`
|
||||
The url of the thumbnail. This can either be an arbitrary URL or an ``attachment://`` URL to work with local files.
|
||||
description: Optional[:class:`str`]
|
||||
The thumbnail's description, up to 1024 characters.
|
||||
spoiler: Optional[:class:`bool`]
|
||||
Whether the thumbnail has the spoiler overlay. Defaults to ``False``.
|
||||
id: Optional[:class:`int`]
|
||||
The thumbnail's ID.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: tuple[str, ...] = (
|
||||
"url",
|
||||
"description",
|
||||
"spoiler",
|
||||
"id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
description: str = None,
|
||||
spoiler: bool = False,
|
||||
id: int = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
media = UnfurledMediaItem(url)
|
||||
|
||||
self._underlying = self._generate_underlying(
|
||||
id=id,
|
||||
media=media,
|
||||
description=description,
|
||||
spoiler=spoiler,
|
||||
)
|
||||
|
||||
def _generate_underlying(
|
||||
self,
|
||||
media: UnfurledMediaItem | None = None,
|
||||
description: str | None = None,
|
||||
spoiler: bool | None = False,
|
||||
id: int | None = None,
|
||||
) -> ThumbnailComponent:
|
||||
super()._generate_underlying(ThumbnailComponent)
|
||||
return ThumbnailComponent._raw_construct(
|
||||
type=ComponentType.thumbnail,
|
||||
id=id or self.id,
|
||||
media=media or self.media,
|
||||
description=description or self.description,
|
||||
spoiler=spoiler if spoiler is not None else self.spoiler,
|
||||
)
|
||||
|
||||
@property
|
||||
def media(self) -> UnfurledMediaItem:
|
||||
"""The thumbnail's unerlying media item."""
|
||||
return self.underlying.media
|
||||
|
||||
@media.setter
|
||||
def media(self, value: UnfurledMediaItem) -> None:
|
||||
self.underlying.media = value
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""The URL of this thumbnail's media. This can either be an arbitrary URL or an ``attachment://`` URL."""
|
||||
return self.underlying.media and self.underlying.media.url
|
||||
|
||||
@url.setter
|
||||
def url(self, value: str) -> None:
|
||||
self.underlying.media.url = value
|
||||
|
||||
@property
|
||||
def description(self) -> str | None:
|
||||
"""The thumbnail's description, up to 1024 characters."""
|
||||
return self.underlying.description
|
||||
|
||||
@description.setter
|
||||
def description(self, description: str | None) -> None:
|
||||
self.underlying.description = description
|
||||
|
||||
@property
|
||||
def spoiler(self) -> bool:
|
||||
"""Whether the thumbnail has the spoiler overlay. Defaults to ``False``."""
|
||||
|
||||
return self.underlying.spoiler
|
||||
|
||||
@spoiler.setter
|
||||
def spoiler(self, spoiler: bool) -> None:
|
||||
self.underlying.spoiler = spoiler
|
||||
|
||||
def to_component_dict(self) -> ThumbnailComponentPayload:
|
||||
return super().to_component_dict()
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: type[T], component: ThumbnailComponent) -> T:
|
||||
return cls(
|
||||
component.media and component.media.url,
|
||||
description=component.description,
|
||||
spoiler=component.spoiler,
|
||||
id=component.id,
|
||||
)
|
||||
|
||||
callback = None
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user