mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-08-14 23:30:14 +00:00
Add support for components V2
Co-authored-by: Michael H <michael@michaelhall.tech> Co-authored-by: Soheab <33902984+Soheab@users.noreply.github.com> Co-authored-by: owocado <24418520+owocado@users.noreply.github.com> Co-authored-by: Jay3332 <40323796+jay3332@users.noreply.github.com> Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com>
This commit is contained in:
parent
6ec2e5329b
commit
50caa3c82c
@ -96,7 +96,7 @@ if TYPE_CHECKING:
|
||||
)
|
||||
from .poll import Poll
|
||||
from .threads import Thread
|
||||
from .ui.view import View
|
||||
from .ui.view import BaseView, View, LayoutView
|
||||
from .types.channel import (
|
||||
PermissionOverwrite as PermissionOverwritePayload,
|
||||
Channel as ChannelPayload,
|
||||
@ -1386,6 +1386,38 @@ class Messageable:
|
||||
async def _get_channel(self) -> MessageableChannel:
|
||||
raise NotImplementedError
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
*,
|
||||
file: File = ...,
|
||||
delete_after: float = ...,
|
||||
nonce: Union[str, int] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
reference: Union[Message, MessageReference, PartialMessage] = ...,
|
||||
mention_author: bool = ...,
|
||||
view: LayoutView,
|
||||
suppress_embeds: bool = ...,
|
||||
silent: bool = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
*,
|
||||
files: Sequence[File] = ...,
|
||||
delete_after: float = ...,
|
||||
nonce: Union[str, int] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
reference: Union[Message, MessageReference, PartialMessage] = ...,
|
||||
mention_author: bool = ...,
|
||||
view: LayoutView,
|
||||
suppress_embeds: bool = ...,
|
||||
silent: bool = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
@ -1485,7 +1517,7 @@ class Messageable:
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
reference: Optional[Union[Message, MessageReference, PartialMessage]] = None,
|
||||
mention_author: Optional[bool] = None,
|
||||
view: Optional[View] = None,
|
||||
view: Optional[BaseView] = None,
|
||||
suppress_embeds: bool = False,
|
||||
silent: bool = False,
|
||||
poll: Optional[Poll] = None,
|
||||
@ -1558,7 +1590,7 @@ class Messageable:
|
||||
If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
view: :class:`discord.ui.View`
|
||||
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
|
||||
A Discord UI View to add to the message.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
@ -1656,7 +1688,7 @@ class Messageable:
|
||||
data = await state.http.send_message(channel.id, params=params)
|
||||
|
||||
ret = state.create_message(channel=channel, data=data)
|
||||
if view and not view.is_finished():
|
||||
if view and not view.is_finished() and view.is_dispatchable():
|
||||
state.store_view(view, ret.id)
|
||||
|
||||
if poll:
|
||||
|
@ -100,7 +100,7 @@ if TYPE_CHECKING:
|
||||
from .file import File
|
||||
from .user import ClientUser, User, BaseUser
|
||||
from .guild import Guild, GuildChannel as GuildChannelType
|
||||
from .ui.view import View
|
||||
from .ui.view import BaseView, View, LayoutView
|
||||
from .types.channel import (
|
||||
TextChannel as TextChannelPayload,
|
||||
NewsChannel as NewsChannelPayload,
|
||||
@ -2841,6 +2841,47 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
|
||||
return result
|
||||
|
||||
@overload
|
||||
async def create_thread(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
auto_archive_duration: ThreadArchiveDuration = ...,
|
||||
slowmode_delay: Optional[int] = ...,
|
||||
file: File = ...,
|
||||
files: Sequence[File] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
mention_author: bool = ...,
|
||||
applied_tags: Sequence[ForumTag] = ...,
|
||||
view: LayoutView,
|
||||
suppress_embeds: bool = ...,
|
||||
reason: Optional[str] = ...,
|
||||
) -> ThreadWithMessage:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def create_thread(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
auto_archive_duration: ThreadArchiveDuration = ...,
|
||||
slowmode_delay: Optional[int] = ...,
|
||||
content: Optional[str] = ...,
|
||||
tts: bool = ...,
|
||||
embed: Embed = ...,
|
||||
embeds: Sequence[Embed] = ...,
|
||||
file: File = ...,
|
||||
files: Sequence[File] = ...,
|
||||
stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
mention_author: bool = ...,
|
||||
applied_tags: Sequence[ForumTag] = ...,
|
||||
view: View = ...,
|
||||
suppress_embeds: bool = ...,
|
||||
reason: Optional[str] = ...,
|
||||
) -> ThreadWithMessage:
|
||||
...
|
||||
|
||||
async def create_thread(
|
||||
self,
|
||||
*,
|
||||
@ -2857,7 +2898,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
allowed_mentions: AllowedMentions = MISSING,
|
||||
mention_author: bool = MISSING,
|
||||
applied_tags: Sequence[ForumTag] = MISSING,
|
||||
view: View = MISSING,
|
||||
view: BaseView = MISSING,
|
||||
suppress_embeds: bool = False,
|
||||
reason: Optional[str] = None,
|
||||
) -> ThreadWithMessage:
|
||||
@ -2907,7 +2948,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``.
|
||||
applied_tags: List[:class:`discord.ForumTag`]
|
||||
A list of tags to apply to the thread.
|
||||
view: :class:`discord.ui.View`
|
||||
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
|
||||
A Discord UI View to add to the message.
|
||||
stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]]
|
||||
A list of stickers to upload. Must be a maximum of 3.
|
||||
@ -2983,7 +3024,7 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
|
||||
data = await state.http.start_thread_in_forum(self.id, params=params, reason=reason)
|
||||
thread = Thread(guild=self.guild, state=self._state, data=data)
|
||||
message = Message(state=self._state, channel=thread, data=data['message'])
|
||||
if view and not view.is_finished():
|
||||
if view and not view.is_finished() and view.is_dispatchable():
|
||||
self._state.store_view(view, message.id)
|
||||
|
||||
return ThreadWithMessage(thread=thread, message=message)
|
||||
|
@ -72,7 +72,7 @@ from .object import Object
|
||||
from .backoff import ExponentialBackoff
|
||||
from .webhook import Webhook
|
||||
from .appinfo import AppInfo
|
||||
from .ui.view import View
|
||||
from .ui.view import BaseView
|
||||
from .ui.dynamic import DynamicItem
|
||||
from .stage_instance import StageInstance
|
||||
from .threads import Thread
|
||||
@ -3156,7 +3156,7 @@ class Client:
|
||||
|
||||
self._connection.remove_dynamic_items(*items)
|
||||
|
||||
def add_view(self, view: View, *, message_id: Optional[int] = None) -> None:
|
||||
def add_view(self, view: BaseView, *, message_id: Optional[int] = None) -> None:
|
||||
"""Registers a :class:`~discord.ui.View` for persistent listening.
|
||||
|
||||
This method should be used for when a view is comprised of components
|
||||
@ -3166,7 +3166,7 @@ class Client:
|
||||
|
||||
Parameters
|
||||
------------
|
||||
view: :class:`discord.ui.View`
|
||||
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
|
||||
The view to register for dispatching.
|
||||
message_id: Optional[:class:`int`]
|
||||
The message ID that the view is attached to. This is currently used to
|
||||
@ -3182,7 +3182,7 @@ class Client:
|
||||
and all their components have an explicitly provided custom_id.
|
||||
"""
|
||||
|
||||
if not isinstance(view, View):
|
||||
if not isinstance(view, BaseView):
|
||||
raise TypeError(f'expected an instance of View not {view.__class__.__name__}')
|
||||
|
||||
if not view.is_persistent():
|
||||
@ -3194,8 +3194,8 @@ class Client:
|
||||
self._connection.store_view(view, message_id)
|
||||
|
||||
@property
|
||||
def persistent_views(self) -> Sequence[View]:
|
||||
"""Sequence[:class:`.View`]: A sequence of persistent views added to the client.
|
||||
def persistent_views(self) -> Sequence[BaseView]:
|
||||
"""Sequence[Union[:class:`.View`, :class:`.LayoutView`]]: A sequence of persistent views added to the client.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
@ -24,9 +24,30 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import ClassVar, List, Literal, Optional, TYPE_CHECKING, Tuple, Union, overload
|
||||
from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, ChannelType, SelectDefaultValueType
|
||||
from .utils import get_slots, MISSING
|
||||
from typing import (
|
||||
ClassVar,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from .asset import AssetMixin
|
||||
from .enums import (
|
||||
try_enum,
|
||||
ComponentType,
|
||||
ButtonStyle,
|
||||
TextStyle,
|
||||
ChannelType,
|
||||
SelectDefaultValueType,
|
||||
SeparatorSpacing,
|
||||
MediaItemLoadingState,
|
||||
)
|
||||
from .flags import AttachmentFlags
|
||||
from .colour import Colour
|
||||
from .utils import get_slots, MISSING, _get_as_snowflake
|
||||
from .partial_emoji import PartialEmoji, _EmojiTag
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -39,13 +60,35 @@ if TYPE_CHECKING:
|
||||
SelectOption as SelectOptionPayload,
|
||||
ActionRow as ActionRowPayload,
|
||||
TextInput as TextInputPayload,
|
||||
ActionRowChildComponent as ActionRowChildComponentPayload,
|
||||
SelectDefaultValues as SelectDefaultValuesPayload,
|
||||
SectionComponent as SectionComponentPayload,
|
||||
TextComponent as TextComponentPayload,
|
||||
MediaGalleryComponent as MediaGalleryComponentPayload,
|
||||
FileComponent as FileComponentPayload,
|
||||
SeparatorComponent as SeparatorComponentPayload,
|
||||
MediaGalleryItem as MediaGalleryItemPayload,
|
||||
ThumbnailComponent as ThumbnailComponentPayload,
|
||||
ContainerComponent as ContainerComponentPayload,
|
||||
UnfurledMediaItem as UnfurledMediaItemPayload,
|
||||
)
|
||||
|
||||
from .emoji import Emoji
|
||||
from .abc import Snowflake
|
||||
from .state import ConnectionState
|
||||
|
||||
ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput']
|
||||
SectionComponentType = Union['TextDisplay']
|
||||
MessageComponentType = Union[
|
||||
ActionRowChildComponentType,
|
||||
SectionComponentType,
|
||||
'ActionRow',
|
||||
'SectionComponent',
|
||||
'ThumbnailComponent',
|
||||
'MediaGalleryComponent',
|
||||
'FileComponent',
|
||||
'SectionComponent',
|
||||
'Component',
|
||||
]
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -56,18 +99,35 @@ __all__ = (
|
||||
'SelectOption',
|
||||
'TextInput',
|
||||
'SelectDefaultValue',
|
||||
'SectionComponent',
|
||||
'ThumbnailComponent',
|
||||
'UnfurledMediaItem',
|
||||
'MediaGalleryItem',
|
||||
'MediaGalleryComponent',
|
||||
'FileComponent',
|
||||
'SectionComponent',
|
||||
'Container',
|
||||
'TextDisplay',
|
||||
'SeparatorComponent',
|
||||
)
|
||||
|
||||
|
||||
class Component:
|
||||
"""Represents a Discord Bot UI Kit Component.
|
||||
|
||||
Currently, the only components supported by Discord are:
|
||||
The components supported by Discord are:
|
||||
|
||||
- :class:`ActionRow`
|
||||
- :class:`Button`
|
||||
- :class:`SelectMenu`
|
||||
- :class:`TextInput`
|
||||
- :class:`SectionComponent`
|
||||
- :class:`TextDisplay`
|
||||
- :class:`ThumbnailComponent`
|
||||
- :class:`MediaGalleryComponent`
|
||||
- :class:`FileComponent`
|
||||
- :class:`SeparatorComponent`
|
||||
- :class:`Container`
|
||||
|
||||
This class is abstract and cannot be instantiated.
|
||||
|
||||
@ -116,20 +176,25 @@ class ActionRow(Component):
|
||||
------------
|
||||
children: List[Union[:class:`Button`, :class:`SelectMenu`, :class:`TextInput`]]
|
||||
The children components that this holds, if any.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = ('children',)
|
||||
__slots__: Tuple[str, ...] = ('children', 'id')
|
||||
|
||||
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||
|
||||
def __init__(self, data: ActionRowPayload, /) -> None:
|
||||
self.id: Optional[int] = data.get('id')
|
||||
self.children: List[ActionRowChildComponentType] = []
|
||||
|
||||
for component_data in data.get('components', []):
|
||||
component = _component_factory(component_data)
|
||||
|
||||
if component is not None:
|
||||
self.children.append(component)
|
||||
self.children.append(component) # type: ignore # should be the correct type here
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.action_row]:
|
||||
@ -137,10 +202,13 @@ class ActionRow(Component):
|
||||
return ComponentType.action_row
|
||||
|
||||
def to_dict(self) -> ActionRowPayload:
|
||||
return {
|
||||
payload: ActionRowPayload = {
|
||||
'type': self.type.value,
|
||||
'components': [child.to_dict() for child in self.children],
|
||||
}
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
return payload
|
||||
|
||||
|
||||
class Button(Component):
|
||||
@ -174,6 +242,10 @@ class Button(Component):
|
||||
The SKU ID this button sends you to, if available.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
@ -184,11 +256,13 @@ class Button(Component):
|
||||
'label',
|
||||
'emoji',
|
||||
'sku_id',
|
||||
'id',
|
||||
)
|
||||
|
||||
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||
|
||||
def __init__(self, data: ButtonComponentPayload, /) -> None:
|
||||
self.id: Optional[int] = data.get('id')
|
||||
self.style: ButtonStyle = try_enum(ButtonStyle, data['style'])
|
||||
self.custom_id: Optional[str] = data.get('custom_id')
|
||||
self.url: Optional[str] = data.get('url')
|
||||
@ -217,6 +291,9 @@ class Button(Component):
|
||||
'disabled': self.disabled,
|
||||
}
|
||||
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
|
||||
if self.sku_id:
|
||||
payload['sku_id'] = str(self.sku_id)
|
||||
|
||||
@ -268,6 +345,10 @@ class SelectMenu(Component):
|
||||
Whether the select is disabled or not.
|
||||
channel_types: List[:class:`.ChannelType`]
|
||||
A list of channel types that are allowed to be chosen in this select menu.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
@ -280,6 +361,7 @@ class SelectMenu(Component):
|
||||
'disabled',
|
||||
'channel_types',
|
||||
'default_values',
|
||||
'id',
|
||||
)
|
||||
|
||||
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||
@ -296,6 +378,7 @@ class SelectMenu(Component):
|
||||
self.default_values: List[SelectDefaultValue] = [
|
||||
SelectDefaultValue.from_dict(d) for d in data.get('default_values', [])
|
||||
]
|
||||
self.id: Optional[int] = data.get('id')
|
||||
|
||||
def to_dict(self) -> SelectMenuPayload:
|
||||
payload: SelectMenuPayload = {
|
||||
@ -305,6 +388,8 @@ class SelectMenu(Component):
|
||||
'max_values': self.max_values,
|
||||
'disabled': self.disabled,
|
||||
}
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
if self.placeholder:
|
||||
payload['placeholder'] = self.placeholder
|
||||
if self.options:
|
||||
@ -312,7 +397,7 @@ class SelectMenu(Component):
|
||||
if self.channel_types:
|
||||
payload['channel_types'] = [t.value for t in self.channel_types]
|
||||
if self.default_values:
|
||||
payload["default_values"] = [v.to_dict() for v in self.default_values]
|
||||
payload['default_values'] = [v.to_dict() for v in self.default_values]
|
||||
|
||||
return payload
|
||||
|
||||
@ -473,6 +558,10 @@ class TextInput(Component):
|
||||
The minimum length of the text input.
|
||||
max_length: Optional[:class:`int`]
|
||||
The maximum length of the text input.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
@ -484,6 +573,7 @@ class TextInput(Component):
|
||||
'required',
|
||||
'min_length',
|
||||
'max_length',
|
||||
'id',
|
||||
)
|
||||
|
||||
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||
@ -497,6 +587,7 @@ class TextInput(Component):
|
||||
self.required: bool = data.get('required', True)
|
||||
self.min_length: Optional[int] = data.get('min_length')
|
||||
self.max_length: Optional[int] = data.get('max_length')
|
||||
self.id: Optional[int] = data.get('id')
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.text_input]:
|
||||
@ -512,6 +603,9 @@ class TextInput(Component):
|
||||
'required': self.required,
|
||||
}
|
||||
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
|
||||
if self.placeholder:
|
||||
payload['placeholder'] = self.placeholder
|
||||
|
||||
@ -645,17 +739,577 @@ class SelectDefaultValue:
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def _component_factory(data: ActionRowChildComponentPayload) -> Optional[ActionRowChildComponentType]:
|
||||
...
|
||||
class SectionComponent(Component):
|
||||
"""Represents a section from the Discord Bot UI Kit.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type to create a section is :class:`discord.ui.Section`
|
||||
not this one.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Attributes
|
||||
----------
|
||||
children: List[:class:`TextDisplay`]
|
||||
The components on this section.
|
||||
accessory: :class:`Component`
|
||||
The section accessory.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'children',
|
||||
'accessory',
|
||||
'id',
|
||||
)
|
||||
|
||||
__repr_info__ = __slots__
|
||||
|
||||
def __init__(self, data: SectionComponentPayload, state: Optional[ConnectionState]) -> None:
|
||||
self.children: List[SectionComponentType] = []
|
||||
self.accessory: Component = _component_factory(data['accessory'], state) # type: ignore
|
||||
self.id: Optional[int] = data.get('id')
|
||||
|
||||
for component_data in data['components']:
|
||||
component = _component_factory(component_data, state)
|
||||
if component is not None:
|
||||
self.children.append(component) # type: ignore # should be the correct type here
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.section]:
|
||||
return ComponentType.section
|
||||
|
||||
def to_dict(self) -> SectionComponentPayload:
|
||||
payload: SectionComponentPayload = {
|
||||
'type': self.type.value,
|
||||
'components': [c.to_dict() for c in self.children],
|
||||
'accessory': self.accessory.to_dict(),
|
||||
}
|
||||
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
@overload
|
||||
def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]:
|
||||
...
|
||||
class ThumbnailComponent(Component):
|
||||
"""Represents a Thumbnail from the Discord Bot UI Kit.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type to create a thumbnail is :class:`discord.ui.Thumbnail`
|
||||
not this one.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Attributes
|
||||
----------
|
||||
media: :class:`UnfurledMediaItem`
|
||||
The media for this thumbnail.
|
||||
description: Optional[:class:`str`]
|
||||
The description shown within this thumbnail.
|
||||
spoiler: :class:`bool`
|
||||
Whether this thumbnail is flagged as a spoiler.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'media',
|
||||
'spoiler',
|
||||
'description',
|
||||
'id',
|
||||
)
|
||||
|
||||
__repr_info__ = __slots__
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: ThumbnailComponentPayload,
|
||||
state: Optional[ConnectionState],
|
||||
) -> None:
|
||||
self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['media'], state)
|
||||
self.description: Optional[str] = data.get('description')
|
||||
self.spoiler: bool = data.get('spoiler', False)
|
||||
self.id: Optional[int] = data.get('id')
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.thumbnail]:
|
||||
return ComponentType.thumbnail
|
||||
|
||||
def to_dict(self) -> ThumbnailComponentPayload:
|
||||
payload = {
|
||||
'media': self.media.to_dict(),
|
||||
'description': self.description,
|
||||
'spoiler': self.spoiler,
|
||||
'type': self.type.value,
|
||||
}
|
||||
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
|
||||
return payload # type: ignore
|
||||
|
||||
|
||||
def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]:
|
||||
class TextDisplay(Component):
|
||||
"""Represents a text display from the Discord Bot UI Kit.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type to create a text display is
|
||||
:class:`discord.ui.TextDisplay` not this one.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Attributes
|
||||
----------
|
||||
content: :class:`str`
|
||||
The content that this display shows.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
"""
|
||||
|
||||
__slots__ = ('content', 'id')
|
||||
|
||||
__repr_info__ = __slots__
|
||||
|
||||
def __init__(self, data: TextComponentPayload) -> None:
|
||||
self.content: str = data['content']
|
||||
self.id: Optional[int] = data.get('id')
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.text_display]:
|
||||
return ComponentType.text_display
|
||||
|
||||
def to_dict(self) -> TextComponentPayload:
|
||||
payload: TextComponentPayload = {
|
||||
'type': self.type.value,
|
||||
'content': self.content,
|
||||
}
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
return payload
|
||||
|
||||
|
||||
class UnfurledMediaItem(AssetMixin):
|
||||
"""Represents an unfurled media item.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url: :class:`str`
|
||||
The URL of this media item. This can be an arbitrary url or a reference to a local
|
||||
file uploaded as an attachment within the message, which can be accessed with the
|
||||
``attachment://<filename>`` format.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
url: :class:`str`
|
||||
The URL of this media item.
|
||||
proxy_url: Optional[:class:`str`]
|
||||
The proxy URL. This is a cached version of the :attr:`.url` in the
|
||||
case of images. When the message is deleted, this URL might be valid for a few minutes
|
||||
or not valid at all.
|
||||
height: Optional[:class:`int`]
|
||||
The media item's height, in pixels. Only applicable to images and videos.
|
||||
width: Optional[:class:`int`]
|
||||
The media item's width, in pixels. Only applicable to images and videos.
|
||||
content_type: Optional[:class:`str`]
|
||||
The media item's `media type <https://en.wikipedia.org/wiki/Media_type>`_
|
||||
placeholder: Optional[:class:`str`]
|
||||
The media item's placeholder.
|
||||
loading_state: Optional[:class:`MediaItemLoadingState`]
|
||||
The loading state of this media item.
|
||||
attachment_id: Optional[:class:`int`]
|
||||
The attachment id this media item points to, only available if the url points to a local file
|
||||
uploaded within the component message.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'url',
|
||||
'proxy_url',
|
||||
'height',
|
||||
'width',
|
||||
'content_type',
|
||||
'_flags',
|
||||
'placeholder',
|
||||
'loading_state',
|
||||
'attachment_id',
|
||||
'_state',
|
||||
)
|
||||
|
||||
def __init__(self, url: str) -> None:
|
||||
self.url: str = url
|
||||
|
||||
self.proxy_url: Optional[str] = None
|
||||
self.height: Optional[int] = None
|
||||
self.width: Optional[int] = None
|
||||
self.content_type: Optional[str] = None
|
||||
self._flags: int = 0
|
||||
self.placeholder: Optional[str] = None
|
||||
self.loading_state: Optional[MediaItemLoadingState] = None
|
||||
self.attachment_id: Optional[int] = None
|
||||
self._state: Optional[ConnectionState] = None
|
||||
|
||||
@property
|
||||
def flags(self) -> AttachmentFlags:
|
||||
""":class:`AttachmentFlags`: This media item's flags."""
|
||||
return AttachmentFlags._from_value(self._flags)
|
||||
|
||||
@classmethod
|
||||
def _from_data(cls, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]):
|
||||
self = cls(data['url'])
|
||||
self._update(data, state)
|
||||
return self
|
||||
|
||||
def _update(self, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]) -> None:
|
||||
self.proxy_url = data.get('proxy_url')
|
||||
self.height = data.get('height')
|
||||
self.width = data.get('width')
|
||||
self.content_type = data.get('content_type')
|
||||
self._flags = data.get('flags', 0)
|
||||
self.placeholder = data.get('placeholder')
|
||||
|
||||
loading_state = data.get('loading_state')
|
||||
if loading_state is not None:
|
||||
self.loading_state = try_enum(MediaItemLoadingState, loading_state)
|
||||
self.attachment_id = _get_as_snowflake(data, 'attachment_id')
|
||||
self._state = state
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<UnfurledMediaItem url={self.url}>'
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'url': self.url,
|
||||
}
|
||||
|
||||
|
||||
class MediaGalleryItem:
|
||||
"""Represents a :class:`MediaGalleryComponent` media item.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
media: Union[:class:`str`, :class:`UnfurledMediaItem`]
|
||||
The media item data. This can be a string representing a local
|
||||
file uploaded as an attachment in the message, which can be accessed
|
||||
using the ``attachment://<filename>`` format, or an arbitrary url.
|
||||
description: Optional[:class:`str`]
|
||||
The description to show within this item. Up to 256 characters. Defaults
|
||||
to ``None``.
|
||||
spoiler: :class:`bool`
|
||||
Whether this item should be flagged as a spoiler.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_media',
|
||||
'description',
|
||||
'spoiler',
|
||||
'_state',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
media: Union[str, UnfurledMediaItem],
|
||||
*,
|
||||
description: Optional[str] = None,
|
||||
spoiler: bool = False,
|
||||
) -> None:
|
||||
self._media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media
|
||||
self.description: Optional[str] = description
|
||||
self.spoiler: bool = spoiler
|
||||
self._state: Optional[ConnectionState] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<MediaGalleryItem media={self.media!r}>'
|
||||
|
||||
@property
|
||||
def media(self) -> UnfurledMediaItem:
|
||||
""":class:`UnfurledMediaItem`: This item's media data."""
|
||||
return self._media
|
||||
|
||||
@media.setter
|
||||
def media(self, value: Union[str, UnfurledMediaItem]) -> None:
|
||||
if isinstance(value, str):
|
||||
self._media = UnfurledMediaItem(value)
|
||||
elif isinstance(value, UnfurledMediaItem):
|
||||
self._media = value
|
||||
else:
|
||||
raise TypeError(f'Expected a str or UnfurledMediaItem, not {value.__class__.__name__}')
|
||||
|
||||
@classmethod
|
||||
def _from_data(cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]) -> MediaGalleryItem:
|
||||
media = data['media']
|
||||
self = cls(
|
||||
media=UnfurledMediaItem._from_data(media, state),
|
||||
description=data.get('description'),
|
||||
spoiler=data.get('spoiler', False),
|
||||
)
|
||||
self._state = state
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def _from_gallery(
|
||||
cls,
|
||||
items: List[MediaGalleryItemPayload],
|
||||
state: Optional[ConnectionState],
|
||||
) -> List[MediaGalleryItem]:
|
||||
return [cls._from_data(item, state) for item in items]
|
||||
|
||||
def to_dict(self) -> MediaGalleryItemPayload:
|
||||
payload: MediaGalleryItemPayload = {
|
||||
'media': self.media.to_dict(), # type: ignore
|
||||
'spoiler': self.spoiler,
|
||||
}
|
||||
|
||||
if self.description:
|
||||
payload['description'] = self.description
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class MediaGalleryComponent(Component):
|
||||
"""Represents a Media Gallery component from the Discord Bot UI Kit.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type for creating a media gallery is
|
||||
:class:`discord.ui.MediaGallery` not this one.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Attributes
|
||||
----------
|
||||
items: List[:class:`MediaGalleryItem`]
|
||||
The items this gallery has.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
"""
|
||||
|
||||
__slots__ = ('items', 'id')
|
||||
|
||||
__repr_info__ = __slots__
|
||||
|
||||
def __init__(self, data: MediaGalleryComponentPayload, state: Optional[ConnectionState]) -> None:
|
||||
self.items: List[MediaGalleryItem] = MediaGalleryItem._from_gallery(data['items'], state)
|
||||
self.id: Optional[int] = data.get('id')
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.media_gallery]:
|
||||
return ComponentType.media_gallery
|
||||
|
||||
def to_dict(self) -> MediaGalleryComponentPayload:
|
||||
payload: MediaGalleryComponentPayload = {
|
||||
'type': self.type.value,
|
||||
'items': [item.to_dict() for item in self.items],
|
||||
}
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
return payload
|
||||
|
||||
|
||||
class FileComponent(Component):
|
||||
"""Represents a File component from the Discord Bot UI Kit.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type for create a file component is
|
||||
:class:`discord.ui.File` not this one.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Attributes
|
||||
----------
|
||||
media: :class:`UnfurledMediaItem`
|
||||
The unfurled attachment contents of the file.
|
||||
spoiler: :class:`bool`
|
||||
Whether this file is flagged as a spoiler.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
name: Optional[:class:`str`]
|
||||
The displayed file name, only available when received from the API.
|
||||
size: Optional[:class:`int`]
|
||||
The file size in MiB, only available when received from the API.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'media',
|
||||
'spoiler',
|
||||
'id',
|
||||
'name',
|
||||
'size',
|
||||
)
|
||||
|
||||
__repr_info__ = __slots__
|
||||
|
||||
def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None:
|
||||
self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['file'], state)
|
||||
self.spoiler: bool = data.get('spoiler', False)
|
||||
self.id: Optional[int] = data.get('id')
|
||||
self.name: Optional[str] = data.get('name')
|
||||
self.size: Optional[int] = data.get('size')
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.file]:
|
||||
return ComponentType.file
|
||||
|
||||
def to_dict(self) -> FileComponentPayload:
|
||||
payload: FileComponentPayload = {
|
||||
'type': self.type.value,
|
||||
'file': self.media.to_dict(), # type: ignore
|
||||
'spoiler': self.spoiler,
|
||||
}
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
return payload
|
||||
|
||||
|
||||
class SeparatorComponent(Component):
|
||||
"""Represents a Separator from the Discord Bot UI Kit.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type for creating a separator is
|
||||
:class:`discord.ui.Separator` not this one.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Attributes
|
||||
----------
|
||||
spacing: :class:`SeparatorSpacing`
|
||||
The spacing size of the separator.
|
||||
visible: :class:`bool`
|
||||
Whether this separator is visible and shows a divider.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'spacing',
|
||||
'visible',
|
||||
'id',
|
||||
)
|
||||
|
||||
__repr_info__ = __slots__
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: SeparatorComponentPayload,
|
||||
) -> None:
|
||||
self.spacing: SeparatorSpacing = try_enum(SeparatorSpacing, data.get('spacing', 1))
|
||||
self.visible: bool = data.get('divider', True)
|
||||
self.id: Optional[int] = data.get('id')
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.separator]:
|
||||
return ComponentType.separator
|
||||
|
||||
def to_dict(self) -> SeparatorComponentPayload:
|
||||
payload: SeparatorComponentPayload = {
|
||||
'type': self.type.value,
|
||||
'divider': self.visible,
|
||||
'spacing': self.spacing.value,
|
||||
}
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
return payload
|
||||
|
||||
|
||||
class Container(Component):
|
||||
"""Represents a Container from the Discord Bot UI Kit.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type for creating a container is
|
||||
:class:`discord.ui.Container` not this one.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Attributes
|
||||
----------
|
||||
children: :class:`Component`
|
||||
This container's children.
|
||||
spoiler: :class:`bool`
|
||||
Whether this container is flagged as a spoiler.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'children',
|
||||
'id',
|
||||
'spoiler',
|
||||
'_colour',
|
||||
)
|
||||
|
||||
__repr_info__ = (
|
||||
'children',
|
||||
'id',
|
||||
'spoiler',
|
||||
'accent_colour',
|
||||
)
|
||||
|
||||
def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionState]) -> None:
|
||||
self.children: List[Component] = []
|
||||
self.id: Optional[int] = data.get('id')
|
||||
|
||||
for child in data['components']:
|
||||
comp = _component_factory(child, state)
|
||||
|
||||
if comp:
|
||||
self.children.append(comp)
|
||||
|
||||
self.spoiler: bool = data.get('spoiler', False)
|
||||
|
||||
colour = data.get('accent_color')
|
||||
self._colour: Optional[Colour] = None
|
||||
if colour is not None:
|
||||
self._colour = Colour(colour)
|
||||
|
||||
@property
|
||||
def accent_colour(self) -> Optional[Colour]:
|
||||
"""Optional[:class:`Colour`]: The container's accent colour."""
|
||||
return self._colour
|
||||
|
||||
accent_color = accent_colour
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.container]:
|
||||
return ComponentType.container
|
||||
|
||||
def to_dict(self) -> ContainerComponentPayload:
|
||||
payload: ContainerComponentPayload = {
|
||||
'type': self.type.value,
|
||||
'spoiler': self.spoiler,
|
||||
'components': [c.to_dict() for c in self.children], # pyright: ignore[reportAssignmentType]
|
||||
}
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
if self._colour:
|
||||
payload['accent_color'] = self._colour.value
|
||||
return payload
|
||||
|
||||
|
||||
def _component_factory(data: ComponentPayload, state: Optional[ConnectionState] = None) -> Optional[Component]:
|
||||
if data['type'] == 1:
|
||||
return ActionRow(data)
|
||||
elif data['type'] == 2:
|
||||
@ -663,4 +1317,18 @@ def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, Acti
|
||||
elif data['type'] == 4:
|
||||
return TextInput(data)
|
||||
elif data['type'] in (3, 5, 6, 7, 8):
|
||||
return SelectMenu(data)
|
||||
return SelectMenu(data) # type: ignore
|
||||
elif data['type'] == 9:
|
||||
return SectionComponent(data, state)
|
||||
elif data['type'] == 10:
|
||||
return TextDisplay(data)
|
||||
elif data['type'] == 11:
|
||||
return ThumbnailComponent(data, state)
|
||||
elif data['type'] == 12:
|
||||
return MediaGalleryComponent(data, state)
|
||||
elif data['type'] == 13:
|
||||
return FileComponent(data, state)
|
||||
elif data['type'] == 14:
|
||||
return SeparatorComponent(data)
|
||||
elif data['type'] == 17:
|
||||
return Container(data, state)
|
||||
|
@ -81,6 +81,8 @@ __all__ = (
|
||||
'StatusDisplayType',
|
||||
'OnboardingPromptType',
|
||||
'OnboardingMode',
|
||||
'SeparatorSpacing',
|
||||
'MediaItemLoadingState',
|
||||
)
|
||||
|
||||
|
||||
@ -668,6 +670,13 @@ class ComponentType(Enum):
|
||||
role_select = 6
|
||||
mentionable_select = 7
|
||||
channel_select = 8
|
||||
section = 9
|
||||
text_display = 10
|
||||
thumbnail = 11
|
||||
media_gallery = 12
|
||||
file = 13
|
||||
separator = 14
|
||||
container = 17
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
@ -953,6 +962,18 @@ class OnboardingMode(Enum):
|
||||
advanced = 1
|
||||
|
||||
|
||||
class SeparatorSpacing(Enum):
|
||||
small = 1
|
||||
large = 2
|
||||
|
||||
|
||||
class MediaItemLoadingState(Enum):
|
||||
unknown = 0
|
||||
loading = 1
|
||||
loaded = 2
|
||||
not_found = 3
|
||||
|
||||
|
||||
def create_unknown_value(cls: Type[E], val: Any) -> E:
|
||||
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
|
||||
name = f'unknown_{val}'
|
||||
|
@ -48,7 +48,7 @@ if TYPE_CHECKING:
|
||||
from discord.mentions import AllowedMentions
|
||||
from discord.sticker import GuildSticker, StickerItem
|
||||
from discord.message import MessageReference, PartialMessage
|
||||
from discord.ui import View
|
||||
from discord.ui.view import BaseView, View, LayoutView
|
||||
from discord.types.interactions import ApplicationCommandInteractionData
|
||||
from discord.poll import Poll
|
||||
|
||||
@ -628,6 +628,40 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
except CommandError as e:
|
||||
await cmd.on_help_command_error(self, e)
|
||||
|
||||
@overload
|
||||
async def reply(
|
||||
self,
|
||||
*,
|
||||
file: File = ...,
|
||||
delete_after: float = ...,
|
||||
nonce: Union[str, int] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
reference: Union[Message, MessageReference, PartialMessage] = ...,
|
||||
mention_author: bool = ...,
|
||||
view: LayoutView,
|
||||
suppress_embeds: bool = ...,
|
||||
ephemeral: bool = ...,
|
||||
silent: bool = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def reply(
|
||||
self,
|
||||
*,
|
||||
files: Sequence[File] = ...,
|
||||
delete_after: float = ...,
|
||||
nonce: Union[str, int] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
reference: Union[Message, MessageReference, PartialMessage] = ...,
|
||||
mention_author: bool = ...,
|
||||
view: LayoutView,
|
||||
suppress_embeds: bool = ...,
|
||||
ephemeral: bool = ...,
|
||||
silent: bool = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def reply(
|
||||
self,
|
||||
@ -817,6 +851,40 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
if self.interaction:
|
||||
await self.interaction.response.defer(ephemeral=ephemeral)
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
*,
|
||||
file: File = ...,
|
||||
delete_after: float = ...,
|
||||
nonce: Union[str, int] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
reference: Union[Message, MessageReference, PartialMessage] = ...,
|
||||
mention_author: bool = ...,
|
||||
view: LayoutView,
|
||||
suppress_embeds: bool = ...,
|
||||
ephemeral: bool = ...,
|
||||
silent: bool = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
*,
|
||||
files: Sequence[File] = ...,
|
||||
delete_after: float = ...,
|
||||
nonce: Union[str, int] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
reference: Union[Message, MessageReference, PartialMessage] = ...,
|
||||
mention_author: bool = ...,
|
||||
view: LayoutView,
|
||||
suppress_embeds: bool = ...,
|
||||
ephemeral: bool = ...,
|
||||
silent: bool = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
@ -920,7 +988,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
reference: Optional[Union[Message, MessageReference, PartialMessage]] = None,
|
||||
mention_author: Optional[bool] = None,
|
||||
view: Optional[View] = None,
|
||||
view: Optional[BaseView] = None,
|
||||
suppress_embeds: bool = False,
|
||||
ephemeral: bool = False,
|
||||
silent: bool = False,
|
||||
@ -986,7 +1054,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
This is ignored for interaction based contexts.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
view: :class:`discord.ui.View`
|
||||
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
|
||||
A Discord UI View to add to the message.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
@ -500,6 +500,16 @@ class MessageFlags(BaseFlags):
|
||||
"""
|
||||
return 16384
|
||||
|
||||
@flag_value
|
||||
def components_v2(self):
|
||||
""":class:`bool`: Returns ``True`` if the message has Discord's v2 components.
|
||||
|
||||
Does not allow sending any ``content``, ``embed``, ``embeds``, ``stickers``, or ``poll``.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
return 32768
|
||||
|
||||
|
||||
@fill_with_flags()
|
||||
class PublicUserFlags(BaseFlags):
|
||||
|
@ -57,16 +57,16 @@ from .file import File
|
||||
from .mentions import AllowedMentions
|
||||
from . import __version__, utils
|
||||
from .utils import MISSING
|
||||
from .flags import MessageFlags
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .ui.view import View
|
||||
from .ui.view import BaseView
|
||||
from .embeds import Embed
|
||||
from .message import Attachment
|
||||
from .flags import MessageFlags
|
||||
from .poll import Poll
|
||||
|
||||
from .types import (
|
||||
@ -151,7 +151,7 @@ def handle_message_parameters(
|
||||
embed: Optional[Embed] = MISSING,
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
view: Optional[BaseView] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = MISSING,
|
||||
message_reference: Optional[message.MessageReference] = MISSING,
|
||||
stickers: Optional[SnowflakeList] = MISSING,
|
||||
@ -194,6 +194,12 @@ def handle_message_parameters(
|
||||
if view is not MISSING:
|
||||
if view is not None:
|
||||
payload['components'] = view.to_components()
|
||||
|
||||
if view.has_components_v2():
|
||||
if flags is not MISSING:
|
||||
flags.components_v2 = True
|
||||
else:
|
||||
flags = MessageFlags(components_v2=True)
|
||||
else:
|
||||
payload['components'] = []
|
||||
|
||||
|
@ -27,7 +27,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union, List
|
||||
from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union, List, overload
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
@ -76,7 +76,7 @@ if TYPE_CHECKING:
|
||||
from .mentions import AllowedMentions
|
||||
from aiohttp import ClientSession
|
||||
from .embeds import Embed
|
||||
from .ui.view import View
|
||||
from .ui.view import BaseView, View, LayoutView
|
||||
from .app_commands.models import Choice, ChoiceT
|
||||
from .ui.modal import Modal
|
||||
from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, DMChannel, GroupChannel
|
||||
@ -482,7 +482,7 @@ class Interaction(Generic[ClientT]):
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
embed: Optional[Embed] = MISSING,
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
view: Optional[Union[View, LayoutView]] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
poll: Poll = MISSING,
|
||||
) -> InteractionMessage:
|
||||
@ -516,9 +516,15 @@ class Interaction(Generic[ClientT]):
|
||||
allowed_mentions: :class:`AllowedMentions`
|
||||
Controls the mentions being processed in this message.
|
||||
See :meth:`.abc.Messageable.send` for more information.
|
||||
view: Optional[:class:`~discord.ui.View`]
|
||||
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
|
||||
The updated view to update this message with. If ``None`` is passed then
|
||||
the view is removed.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to update the message to have a :class:`~discord.ui.LayoutView`, you must
|
||||
explicitly set the ``content``, ``embed``, ``embeds``, and ``attachments`` parameters to
|
||||
``None`` if the previous message had any.
|
||||
poll: :class:`Poll`
|
||||
The poll to create when editing the message.
|
||||
|
||||
@ -574,7 +580,7 @@ class Interaction(Generic[ClientT]):
|
||||
# The message channel types should always match
|
||||
state = _InteractionMessageState(self, self._state)
|
||||
message = InteractionMessage(state=state, channel=self.channel, data=data) # type: ignore
|
||||
if view and not view.is_finished():
|
||||
if view and not view.is_finished() and view.is_dispatchable():
|
||||
self._state.store_view(view, message.id, interaction_id=self.id)
|
||||
return message
|
||||
|
||||
@ -898,6 +904,22 @@ class InteractionResponse(Generic[ClientT]):
|
||||
)
|
||||
self._response_type = InteractionResponseType.pong
|
||||
|
||||
@overload
|
||||
async def send_message(
|
||||
self,
|
||||
*,
|
||||
file: File = MISSING,
|
||||
files: Sequence[File] = MISSING,
|
||||
view: LayoutView,
|
||||
ephemeral: bool = False,
|
||||
allowed_mentions: AllowedMentions = MISSING,
|
||||
suppress_embeds: bool = False,
|
||||
silent: bool = False,
|
||||
delete_after: Optional[float] = None,
|
||||
) -> InteractionCallbackResponse[ClientT]:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def send_message(
|
||||
self,
|
||||
content: Optional[Any] = None,
|
||||
@ -914,6 +936,25 @@ class InteractionResponse(Generic[ClientT]):
|
||||
silent: bool = False,
|
||||
delete_after: Optional[float] = None,
|
||||
poll: Poll = MISSING,
|
||||
) -> InteractionCallbackResponse[ClientT]:
|
||||
...
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
content: Optional[Any] = None,
|
||||
*,
|
||||
embed: Embed = MISSING,
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
file: File = MISSING,
|
||||
files: Sequence[File] = MISSING,
|
||||
view: BaseView = MISSING,
|
||||
tts: bool = False,
|
||||
ephemeral: bool = False,
|
||||
allowed_mentions: AllowedMentions = MISSING,
|
||||
suppress_embeds: bool = False,
|
||||
silent: bool = False,
|
||||
delete_after: Optional[float] = None,
|
||||
poll: Poll = MISSING,
|
||||
) -> InteractionCallbackResponse[ClientT]:
|
||||
"""|coro|
|
||||
|
||||
@ -938,7 +979,7 @@ class InteractionResponse(Generic[ClientT]):
|
||||
A list of files to upload. Must be a maximum of 10.
|
||||
tts: :class:`bool`
|
||||
Indicates if the message should be sent using text-to-speech.
|
||||
view: :class:`discord.ui.View`
|
||||
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
|
||||
The view to send with the message.
|
||||
ephemeral: :class:`bool`
|
||||
Indicates if the message should only be visible to the user who started the interaction.
|
||||
@ -1055,7 +1096,7 @@ class InteractionResponse(Generic[ClientT]):
|
||||
embed: Optional[Embed] = MISSING,
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
view: Optional[Union[View, LayoutView]] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = MISSING,
|
||||
delete_after: Optional[float] = None,
|
||||
suppress_embeds: bool = MISSING,
|
||||
@ -1085,9 +1126,15 @@ class InteractionResponse(Generic[ClientT]):
|
||||
|
||||
New files will always appear after current attachments.
|
||||
|
||||
view: Optional[:class:`~discord.ui.View`]
|
||||
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
|
||||
The updated view to update this message with. If ``None`` is passed then
|
||||
the view is removed.
|
||||
|
||||
.. note::
|
||||
|
||||
To update the message to add a :class:`~discord.ui.LayoutView`, you
|
||||
must explicitly set the ``content``, ``embed``, ``embeds``, and
|
||||
``attachments`` parameters to either ``None`` or an empty array, as appropriate.
|
||||
allowed_mentions: Optional[:class:`~discord.AllowedMentions`]
|
||||
Controls the mentions being processed in this message. See :meth:`.Message.edit`
|
||||
for more information.
|
||||
@ -1169,7 +1216,7 @@ class InteractionResponse(Generic[ClientT]):
|
||||
params=params,
|
||||
)
|
||||
|
||||
if view and not view.is_finished():
|
||||
if view and not view.is_finished() and view.is_dispatchable():
|
||||
state.store_view(view, message_id, interaction_id=original_interaction_id)
|
||||
|
||||
self._response_type = InteractionResponseType.message_update
|
||||
@ -1382,6 +1429,18 @@ class InteractionMessage(Message):
|
||||
__slots__ = ()
|
||||
_state: _InteractionMessageState
|
||||
|
||||
@overload
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
view: LayoutView,
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
delete_after: Optional[float] = None,
|
||||
) -> InteractionMessage:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
@ -1393,6 +1452,20 @@ class InteractionMessage(Message):
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
delete_after: Optional[float] = None,
|
||||
poll: Poll = MISSING,
|
||||
) -> InteractionMessage:
|
||||
...
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
content: Optional[str] = MISSING,
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
embed: Optional[Embed] = MISSING,
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
view: Optional[Union[View, LayoutView]] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
delete_after: Optional[float] = None,
|
||||
poll: Poll = MISSING,
|
||||
) -> InteractionMessage:
|
||||
"""|coro|
|
||||
|
||||
@ -1418,9 +1491,15 @@ class InteractionMessage(Message):
|
||||
allowed_mentions: :class:`AllowedMentions`
|
||||
Controls the mentions being processed in this message.
|
||||
See :meth:`.abc.Messageable.send` for more information.
|
||||
view: Optional[:class:`~discord.ui.View`]
|
||||
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
|
||||
The updated view to update this message with. If ``None`` is passed then
|
||||
the view is removed.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to update the message to have a :class:`~discord.ui.LayoutView`, you must
|
||||
explicitly set the ``content``, ``embed``, ``embeds``, and ``attachments`` parameters to
|
||||
``None`` if the previous message had any.
|
||||
delete_after: Optional[:class:`float`]
|
||||
If provided, the number of seconds to wait in the background
|
||||
before deleting the message we just sent. If the deletion fails,
|
||||
|
@ -96,15 +96,14 @@ if TYPE_CHECKING:
|
||||
from .types.gateway import MessageReactionRemoveEvent, MessageUpdateEvent
|
||||
from .abc import Snowflake
|
||||
from .abc import GuildChannel, MessageableChannel
|
||||
from .components import ActionRow, ActionRowChildComponentType
|
||||
from .components import MessageComponentType
|
||||
from .state import ConnectionState
|
||||
from .mentions import AllowedMentions
|
||||
from .user import User
|
||||
from .role import Role
|
||||
from .ui.view import View
|
||||
from .ui.view import View, LayoutView
|
||||
|
||||
EmojiInputType = Union[Emoji, PartialEmoji, str]
|
||||
MessageComponentType = Union[ActionRow, ActionRowChildComponentType]
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -489,7 +488,7 @@ class MessageSnapshot:
|
||||
Extra features of the the message snapshot.
|
||||
stickers: List[:class:`StickerItem`]
|
||||
A list of sticker items given to the message.
|
||||
components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`]]
|
||||
components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`, :class:`Container`, :class:`SectionComponent`, :class:`TextDisplay`, :class:`MediaGalleryComponent`, :class:`FileComponent`, :class:`SeparatorComponent`, :class:`ThumbnailComponent`]]
|
||||
A list of components in the message.
|
||||
"""
|
||||
|
||||
@ -533,7 +532,7 @@ class MessageSnapshot:
|
||||
|
||||
self.components: List[MessageComponentType] = []
|
||||
for component_data in data.get('components', []):
|
||||
component = _component_factory(component_data)
|
||||
component = _component_factory(component_data, state) # type: ignore
|
||||
if component is not None:
|
||||
self.components.append(component)
|
||||
|
||||
@ -1306,32 +1305,6 @@ class PartialMessage(Hashable):
|
||||
else:
|
||||
await self._state.http.delete_message(self.channel.id, self.id)
|
||||
|
||||
@overload
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
content: Optional[str] = ...,
|
||||
embed: Optional[Embed] = ...,
|
||||
attachments: Sequence[Union[Attachment, File]] = ...,
|
||||
delete_after: Optional[float] = ...,
|
||||
allowed_mentions: Optional[AllowedMentions] = ...,
|
||||
view: Optional[View] = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
content: Optional[str] = ...,
|
||||
embeds: Sequence[Embed] = ...,
|
||||
attachments: Sequence[Union[Attachment, File]] = ...,
|
||||
delete_after: Optional[float] = ...,
|
||||
allowed_mentions: Optional[AllowedMentions] = ...,
|
||||
view: Optional[View] = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
@ -1341,7 +1314,7 @@ class PartialMessage(Hashable):
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
delete_after: Optional[float] = None,
|
||||
allowed_mentions: Optional[AllowedMentions] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
view: Optional[Union[View, LayoutView]] = MISSING,
|
||||
) -> Message:
|
||||
"""|coro|
|
||||
|
||||
@ -1391,10 +1364,16 @@ class PartialMessage(Hashable):
|
||||
are used instead.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
view: Optional[:class:`~discord.ui.View`]
|
||||
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
|
||||
The updated view to update this message with. If ``None`` is passed then
|
||||
the view is removed.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to update the message to have a :class:`~discord.ui.LayoutView`, you must
|
||||
explicitly set the ``content``, ``embed``, ``embeds``, and ``attachments`` parameters to
|
||||
``None`` if the previous message had any.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
@ -1433,8 +1412,8 @@ class PartialMessage(Hashable):
|
||||
data = await self._state.http.edit_message(self.channel.id, self.id, params=params)
|
||||
message = Message(state=self._state, channel=self.channel, data=data)
|
||||
|
||||
if view and not view.is_finished():
|
||||
interaction: Optional[MessageInteraction] = getattr(self, 'interaction', None)
|
||||
if view and not view.is_finished() and view.is_dispatchable():
|
||||
interaction: Optional[MessageInteractionMetadata] = getattr(self, 'interaction_metadata', None)
|
||||
if interaction is not None:
|
||||
self._state.store_view(view, self.id, interaction_id=interaction.id)
|
||||
else:
|
||||
@ -1756,6 +1735,38 @@ class PartialMessage(Hashable):
|
||||
|
||||
return await self.guild.fetch_channel(self.id) # type: ignore # Can only be Thread in this case
|
||||
|
||||
@overload
|
||||
async def reply(
|
||||
self,
|
||||
*,
|
||||
file: File = ...,
|
||||
view: LayoutView,
|
||||
delete_after: float = ...,
|
||||
nonce: Union[str, int] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
reference: Union[Message, MessageReference, PartialMessage] = ...,
|
||||
mention_author: bool = ...,
|
||||
suppress_embeds: bool = ...,
|
||||
silent: bool = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def reply(
|
||||
self,
|
||||
*,
|
||||
files: Sequence[File] = ...,
|
||||
view: LayoutView,
|
||||
delete_after: float = ...,
|
||||
nonce: Union[str, int] = ...,
|
||||
allowed_mentions: AllowedMentions = ...,
|
||||
reference: Union[Message, MessageReference, PartialMessage] = ...,
|
||||
mention_author: bool = ...,
|
||||
suppress_embeds: bool = ...,
|
||||
silent: bool = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def reply(
|
||||
self,
|
||||
@ -2846,34 +2857,6 @@ class Message(PartialMessage, Hashable):
|
||||
# Fallback for unknown message types
|
||||
return ''
|
||||
|
||||
@overload
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
content: Optional[str] = ...,
|
||||
embed: Optional[Embed] = ...,
|
||||
attachments: Sequence[Union[Attachment, File]] = ...,
|
||||
suppress: bool = ...,
|
||||
delete_after: Optional[float] = ...,
|
||||
allowed_mentions: Optional[AllowedMentions] = ...,
|
||||
view: Optional[View] = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
content: Optional[str] = ...,
|
||||
embeds: Sequence[Embed] = ...,
|
||||
attachments: Sequence[Union[Attachment, File]] = ...,
|
||||
suppress: bool = ...,
|
||||
delete_after: Optional[float] = ...,
|
||||
allowed_mentions: Optional[AllowedMentions] = ...,
|
||||
view: Optional[View] = ...,
|
||||
) -> Message:
|
||||
...
|
||||
|
||||
async def edit(
|
||||
self,
|
||||
*,
|
||||
@ -2884,7 +2867,7 @@ class Message(PartialMessage, Hashable):
|
||||
suppress: bool = False,
|
||||
delete_after: Optional[float] = None,
|
||||
allowed_mentions: Optional[AllowedMentions] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
view: Optional[Union[View, LayoutView]] = MISSING,
|
||||
) -> Message:
|
||||
"""|coro|
|
||||
|
||||
@ -2942,10 +2925,16 @@ class Message(PartialMessage, Hashable):
|
||||
are used instead.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
view: Optional[:class:`~discord.ui.View`]
|
||||
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
|
||||
The updated view to update this message with. If ``None`` is passed then
|
||||
the view is removed.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to update the message to have a :class:`~discord.ui.LayoutView`, you must
|
||||
explicitly set the ``content``, ``embed``, ``embeds``, and ``attachments`` parameters to
|
||||
``None`` if the previous message had any.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
@ -2991,7 +2980,7 @@ class Message(PartialMessage, Hashable):
|
||||
data = await self._state.http.edit_message(self.channel.id, self.id, params=params)
|
||||
message = Message(state=self._state, channel=self.channel, data=data)
|
||||
|
||||
if view and not view.is_finished():
|
||||
if view and not view.is_finished() and view.is_dispatchable():
|
||||
self._state.store_view(view, self.id)
|
||||
|
||||
if delete_after is not None:
|
||||
|
@ -71,7 +71,7 @@ from .flags import ApplicationFlags, Intents, MemberCacheFlags
|
||||
from .invite import Invite
|
||||
from .integrations import _integration_factory
|
||||
from .interactions import Interaction
|
||||
from .ui.view import ViewStore, View
|
||||
from .ui.view import ViewStore, BaseView
|
||||
from .scheduled_event import ScheduledEvent
|
||||
from .stage_instance import StageInstance
|
||||
from .threads import Thread, ThreadMember
|
||||
@ -412,12 +412,12 @@ class ConnectionState(Generic[ClientT]):
|
||||
self._stickers[sticker_id] = sticker = GuildSticker(state=self, data=data)
|
||||
return sticker
|
||||
|
||||
def store_view(self, view: View, message_id: Optional[int] = None, interaction_id: Optional[int] = None) -> None:
|
||||
def store_view(self, view: BaseView, message_id: Optional[int] = None, interaction_id: Optional[int] = None) -> None:
|
||||
if interaction_id is not None:
|
||||
self._view_store.remove_interaction_mapping(interaction_id)
|
||||
self._view_store.add_view(view, message_id)
|
||||
|
||||
def prevent_view_updates_for(self, message_id: int) -> Optional[View]:
|
||||
def prevent_view_updates_for(self, message_id: int) -> Optional[BaseView]:
|
||||
return self._view_store.remove_message_tracking(message_id)
|
||||
|
||||
def store_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None:
|
||||
@ -427,7 +427,7 @@ class ConnectionState(Generic[ClientT]):
|
||||
self._view_store.remove_dynamic_items(*items)
|
||||
|
||||
@property
|
||||
def persistent_views(self) -> Sequence[View]:
|
||||
def persistent_views(self) -> Sequence[BaseView]:
|
||||
return self._view_store.persistent_views
|
||||
|
||||
@property
|
||||
|
@ -24,24 +24,31 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Literal, TypedDict, Union
|
||||
from typing import List, Literal, Optional, TypedDict, Union
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
from .emoji import PartialEmoji
|
||||
from .channel import ChannelType
|
||||
|
||||
ComponentType = Literal[1, 2, 3, 4]
|
||||
ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17]
|
||||
ButtonStyle = Literal[1, 2, 3, 4, 5, 6]
|
||||
TextStyle = Literal[1, 2]
|
||||
DefaultValueType = Literal['user', 'role', 'channel']
|
||||
SeparatorSpacing = Literal[1, 2]
|
||||
MediaItemLoadingState = Literal[0, 1, 2, 3]
|
||||
|
||||
|
||||
class ActionRow(TypedDict):
|
||||
class ComponentBase(TypedDict):
|
||||
id: NotRequired[int]
|
||||
type: int
|
||||
|
||||
|
||||
class ActionRow(ComponentBase):
|
||||
type: Literal[1]
|
||||
components: List[ActionRowChildComponent]
|
||||
|
||||
|
||||
class ButtonComponent(TypedDict):
|
||||
class ButtonComponent(ComponentBase):
|
||||
type: Literal[2]
|
||||
style: ButtonStyle
|
||||
custom_id: NotRequired[str]
|
||||
@ -60,7 +67,7 @@ class SelectOption(TypedDict):
|
||||
emoji: NotRequired[PartialEmoji]
|
||||
|
||||
|
||||
class SelectComponent(TypedDict):
|
||||
class SelectComponent(ComponentBase):
|
||||
custom_id: str
|
||||
placeholder: NotRequired[str]
|
||||
min_values: NotRequired[int]
|
||||
@ -99,7 +106,7 @@ class ChannelSelectComponent(SelectComponent):
|
||||
default_values: NotRequired[List[SelectDefaultValues]]
|
||||
|
||||
|
||||
class TextInput(TypedDict):
|
||||
class TextInput(ComponentBase):
|
||||
type: Literal[4]
|
||||
custom_id: str
|
||||
style: TextStyle
|
||||
@ -118,5 +125,78 @@ class SelectMenu(SelectComponent):
|
||||
default_values: NotRequired[List[SelectDefaultValues]]
|
||||
|
||||
|
||||
class SectionComponent(ComponentBase):
|
||||
type: Literal[9]
|
||||
components: List[Union[TextComponent, ButtonComponent]]
|
||||
accessory: Component
|
||||
|
||||
|
||||
class TextComponent(ComponentBase):
|
||||
type: Literal[10]
|
||||
content: str
|
||||
|
||||
|
||||
class UnfurledMediaItem(TypedDict):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: NotRequired[Optional[int]]
|
||||
width: NotRequired[Optional[int]]
|
||||
content_type: NotRequired[str]
|
||||
placeholder: str
|
||||
loading_state: MediaItemLoadingState
|
||||
attachment_id: NotRequired[int]
|
||||
flags: NotRequired[int]
|
||||
|
||||
|
||||
class ThumbnailComponent(ComponentBase):
|
||||
type: Literal[11]
|
||||
media: UnfurledMediaItem
|
||||
description: NotRequired[Optional[str]]
|
||||
spoiler: NotRequired[bool]
|
||||
|
||||
|
||||
class MediaGalleryItem(TypedDict):
|
||||
media: UnfurledMediaItem
|
||||
description: NotRequired[str]
|
||||
spoiler: NotRequired[bool]
|
||||
|
||||
|
||||
class MediaGalleryComponent(ComponentBase):
|
||||
type: Literal[12]
|
||||
items: List[MediaGalleryItem]
|
||||
|
||||
|
||||
class FileComponent(ComponentBase):
|
||||
type: Literal[13]
|
||||
file: UnfurledMediaItem
|
||||
spoiler: NotRequired[bool]
|
||||
name: NotRequired[str]
|
||||
size: NotRequired[int]
|
||||
|
||||
|
||||
class SeparatorComponent(ComponentBase):
|
||||
type: Literal[14]
|
||||
divider: NotRequired[bool]
|
||||
spacing: NotRequired[SeparatorSpacing]
|
||||
|
||||
|
||||
class ContainerComponent(ComponentBase):
|
||||
type: Literal[17]
|
||||
accent_color: NotRequired[int]
|
||||
spoiler: NotRequired[bool]
|
||||
components: List[ContainerChildComponent]
|
||||
|
||||
|
||||
ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput]
|
||||
Component = Union[ActionRow, ActionRowChildComponent]
|
||||
ContainerChildComponent = Union[
|
||||
ActionRow,
|
||||
TextComponent,
|
||||
MediaGalleryComponent,
|
||||
FileComponent,
|
||||
SectionComponent,
|
||||
SectionComponent,
|
||||
ContainerComponent,
|
||||
SeparatorComponent,
|
||||
ThumbnailComponent,
|
||||
]
|
||||
Component = Union[ActionRowChildComponent, ContainerChildComponent]
|
||||
|
@ -33,7 +33,7 @@ from .user import User
|
||||
from .emoji import PartialEmoji
|
||||
from .embed import Embed
|
||||
from .channel import ChannelType
|
||||
from .components import Component
|
||||
from .components import ComponentBase
|
||||
from .interactions import MessageInteraction, MessageInteractionMetadata
|
||||
from .sticker import StickerItem
|
||||
from .threads import Thread
|
||||
@ -189,7 +189,7 @@ class MessageSnapshot(TypedDict):
|
||||
mentions: List[UserWithMember]
|
||||
mention_roles: SnowflakeList
|
||||
sticker_items: NotRequired[List[StickerItem]]
|
||||
components: NotRequired[List[Component]]
|
||||
components: NotRequired[List[ComponentBase]]
|
||||
|
||||
|
||||
class Message(PartialMessage):
|
||||
@ -221,7 +221,7 @@ class Message(PartialMessage):
|
||||
referenced_message: NotRequired[Optional[Message]]
|
||||
interaction: NotRequired[MessageInteraction] # deprecated, use interaction_metadata
|
||||
interaction_metadata: NotRequired[MessageInteractionMetadata]
|
||||
components: NotRequired[List[Component]]
|
||||
components: NotRequired[List[ComponentBase]]
|
||||
position: NotRequired[int]
|
||||
role_subscription_data: NotRequired[RoleSubscriptionData]
|
||||
thread: NotRequired[Thread]
|
||||
|
@ -16,3 +16,11 @@ from .button import *
|
||||
from .select import *
|
||||
from .text_input import *
|
||||
from .dynamic import *
|
||||
from .container import *
|
||||
from .file import *
|
||||
from .media_gallery import *
|
||||
from .section import *
|
||||
from .separator import *
|
||||
from .text_display import *
|
||||
from .thumbnail import *
|
||||
from .action_row import *
|
||||
|
585
discord/ui/action_row.py
Normal file
585
discord/ui/action_row.py
Normal file
@ -0,0 +1,585 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Generator,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
from .item import I, Item
|
||||
from .button import Button, button as _button
|
||||
from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect
|
||||
from ..components import ActionRow as ActionRowComponent
|
||||
from ..enums import ButtonStyle, ComponentType, ChannelType
|
||||
from ..partial_emoji import PartialEmoji
|
||||
from ..utils import MISSING, get as _utils_get
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .view import LayoutView
|
||||
from .select import (
|
||||
BaseSelectT,
|
||||
ValidDefaultValues,
|
||||
MentionableSelectT,
|
||||
ChannelSelectT,
|
||||
RoleSelectT,
|
||||
UserSelectT,
|
||||
SelectT,
|
||||
)
|
||||
from ..emoji import Emoji
|
||||
from ..components import SelectOption
|
||||
from ..interactions import Interaction
|
||||
|
||||
ItemCallbackType = Callable[['S', Interaction[Any], I], Coroutine[Any, Any, Any]]
|
||||
SelectCallbackDecorator = Callable[[ItemCallbackType['S', BaseSelectT]], BaseSelectT]
|
||||
|
||||
S = TypeVar('S', bound='ActionRow', covariant=True)
|
||||
V = TypeVar('V', bound='LayoutView', covariant=True)
|
||||
|
||||
__all__ = ('ActionRow',)
|
||||
|
||||
|
||||
class _ActionRowCallback:
|
||||
__slots__ = ('row', 'callback', 'item')
|
||||
|
||||
def __init__(self, callback: ItemCallbackType[S, Any], row: ActionRow, item: Item[Any]) -> None:
|
||||
self.callback: ItemCallbackType[Any, Any] = callback
|
||||
self.row: ActionRow = row
|
||||
self.item: Item[Any] = item
|
||||
|
||||
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
|
||||
return self.callback(self.row, interaction, self.item)
|
||||
|
||||
|
||||
class ActionRow(Item[V]):
|
||||
r"""Represents a UI action row.
|
||||
|
||||
This is a top-level layout component that can only be used on :class:`LayoutView`
|
||||
and can contain :class:`Button`\s and :class:`Select`\s in it.
|
||||
|
||||
Action rows can only have 5 children. This can be inherited.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
import discord
|
||||
from discord import ui
|
||||
|
||||
# you can subclass it and add components with the decorators
|
||||
class MyActionRow(ui.ActionRow):
|
||||
@ui.button(label='Click Me!')
|
||||
async def click_me(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.send_message('You clicked me!')
|
||||
|
||||
# or use it directly on LayoutView
|
||||
class MyView(ui.LayoutView):
|
||||
row = ui.ActionRow()
|
||||
# or you can use your subclass:
|
||||
# row = MyActionRow()
|
||||
|
||||
# you can add items with row.button and row.select
|
||||
@row.button(label='A button!')
|
||||
async def row_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.send_message('You clicked a button!')
|
||||
|
||||
Parameters
|
||||
----------
|
||||
\*children: :class:`Item`
|
||||
The initial children of this action row.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
"""
|
||||
|
||||
__action_row_children_items__: ClassVar[List[ItemCallbackType[Self, Any]]] = []
|
||||
__discord_ui_action_row__: ClassVar[bool] = True
|
||||
__item_repr_attributes__ = ('id',)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: Item[V],
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._children: List[Item[V]] = self._init_children()
|
||||
self._children.extend(children)
|
||||
self._weight: int = sum(i.width for i in self._children)
|
||||
|
||||
if self._weight > 5:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
|
||||
self.id = id
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
|
||||
children: Dict[str, ItemCallbackType[Self, Any]] = {}
|
||||
for base in reversed(cls.__mro__):
|
||||
for name, member in base.__dict__.items():
|
||||
if hasattr(member, '__discord_ui_model_type__'):
|
||||
children[name] = member
|
||||
|
||||
if len(children) > 5:
|
||||
raise TypeError('ActionRow cannot have more than 5 children')
|
||||
|
||||
cls.__action_row_children_items__ = list(children.values())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} children={len(self._children)}>'
|
||||
|
||||
def _init_children(self) -> List[Item[Any]]:
|
||||
children = []
|
||||
|
||||
for func in self.__action_row_children_items__:
|
||||
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
|
||||
item.callback = _ActionRowCallback(func, self, item) # type: ignore
|
||||
item._parent = getattr(func, '__discord_ui_parent__', self)
|
||||
setattr(self, func.__name__, item)
|
||||
children.append(item)
|
||||
return children
|
||||
|
||||
def _update_view(self, view) -> None:
|
||||
self._view = view
|
||||
for child in self._children:
|
||||
child._view = view
|
||||
|
||||
def _has_children(self):
|
||||
return True
|
||||
|
||||
def _is_v2(self) -> bool:
|
||||
# although it is not really a v2 component the only usecase here is for
|
||||
# LayoutView which basically represents the top-level payload of components
|
||||
# and ActionRow is only allowed there anyways.
|
||||
# If the user tries to add any V2 component to a View instead of LayoutView
|
||||
# it should error anyways.
|
||||
return True
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return 5
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.action_row]:
|
||||
return ComponentType.action_row
|
||||
|
||||
@property
|
||||
def children(self) -> List[Item[V]]:
|
||||
"""List[:class:`Item`]: The list of children attached to this action row."""
|
||||
return self._children.copy()
|
||||
|
||||
def walk_children(self) -> Generator[Item[V], Any, None]:
|
||||
"""An iterator that recursively walks through all the children of this action row
|
||||
and its children, if applicable.
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`Item`
|
||||
An item in the action row.
|
||||
"""
|
||||
|
||||
for child in self.children:
|
||||
yield child
|
||||
|
||||
def add_item(self, item: Item[Any]) -> Self:
|
||||
"""Adds an item to this action row.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`Item`
|
||||
The item to add to the action row.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
An :class:`Item` was not passed.
|
||||
ValueError
|
||||
Maximum number of children has been exceeded (5).
|
||||
"""
|
||||
|
||||
if (self._weight + item.width) > 5:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
|
||||
if len(self._children) >= 5:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
|
||||
if not isinstance(item, Item):
|
||||
raise TypeError(f'expected Item not {item.__class__.__name__}')
|
||||
|
||||
item._update_view(self.view)
|
||||
item._parent = self
|
||||
self._weight += 1
|
||||
self._children.append(item)
|
||||
|
||||
if self._view:
|
||||
self._view._total_children += 1
|
||||
|
||||
return self
|
||||
|
||||
def remove_item(self, item: Item[Any]) -> Self:
|
||||
"""Removes an item from the action row.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`Item`
|
||||
The item to remove from the action row.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._children.remove(item)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if self._view and self._view._is_layout():
|
||||
self._view._total_children -= 1
|
||||
self._weight -= 1
|
||||
|
||||
return self
|
||||
|
||||
def find_item(self, id: int, /) -> Optional[Item[V]]:
|
||||
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
|
||||
not found.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is **not the same** as ``custom_id``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id: :class:`int`
|
||||
The ID of the component.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`Item`]
|
||||
The item found, or ``None``.
|
||||
"""
|
||||
return _utils_get(self.walk_children(), id=id)
|
||||
|
||||
def clear_items(self) -> Self:
|
||||
"""Removes all items from the action row.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
"""
|
||||
if self._view and self._view._is_layout():
|
||||
self._view._total_children -= len(self._children)
|
||||
self._children.clear()
|
||||
self._weight = 0
|
||||
return self
|
||||
|
||||
def to_component_dict(self) -> Dict[str, Any]:
|
||||
components = []
|
||||
for component in self.children:
|
||||
components.append(component.to_component_dict())
|
||||
|
||||
base = {
|
||||
'type': self.type.value,
|
||||
'components': components,
|
||||
}
|
||||
if self.id is not None:
|
||||
base['id'] = self.id
|
||||
return base
|
||||
|
||||
def button(
|
||||
self,
|
||||
*,
|
||||
label: Optional[str] = None,
|
||||
custom_id: Optional[str] = None,
|
||||
disabled: bool = False,
|
||||
style: ButtonStyle = ButtonStyle.secondary,
|
||||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||
id: Optional[int] = None,
|
||||
) -> Callable[[ItemCallbackType[S, Button[V]]], Button[V]]:
|
||||
"""A decorator that attaches a button to the action row.
|
||||
|
||||
The function being decorated should have three parameters, ``self`` representing
|
||||
the :class:`discord.ui.ActionRow`, the :class:`discord.Interaction` you receive and
|
||||
the :class:`discord.ui.Button` being pressed.
|
||||
.. note::
|
||||
|
||||
Buttons with a URL or a SKU cannot be created with this function.
|
||||
Consider creating a :class:`Button` manually and adding it via
|
||||
:meth:`ActionRow.add_item` instead. This is beacuse these buttons
|
||||
cannot have a callback associated with them since Discord does not
|
||||
do any processing with them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label: Optional[:class:`str`]
|
||||
The label of the button, if any.
|
||||
Can only be up to 80 characters.
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the button that gets received during an interaction.
|
||||
It is recommended to not set this parameters to prevent conflicts.
|
||||
Can only be up to 100 characters.
|
||||
style: :class:`.ButtonStyle`
|
||||
The style of the button. Defaults to :attr:`.ButtonStyle.grey`.
|
||||
disabled: :class:`bool`
|
||||
Whether the button is disabled or not. Defaults to ``False``.
|
||||
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
|
||||
The emoji of the button. This can be in string form or a :class:`.PartialEmoji`
|
||||
or a full :class:`.Emoji`.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
def decorator(func: ItemCallbackType[S, Button[V]]) -> ItemCallbackType[S, Button[V]]:
|
||||
ret = _button(
|
||||
label=label,
|
||||
custom_id=custom_id,
|
||||
disabled=disabled,
|
||||
style=style,
|
||||
emoji=emoji,
|
||||
row=None,
|
||||
id=id,
|
||||
)(func)
|
||||
ret.__discord_ui_parent__ = self # type: ignore
|
||||
return ret # type: ignore
|
||||
|
||||
return decorator # type: ignore
|
||||
|
||||
@overload
|
||||
def select(
|
||||
self,
|
||||
*,
|
||||
cls: Type[SelectT] = Select[Any],
|
||||
options: List[SelectOption] = MISSING,
|
||||
channel_types: List[ChannelType] = ...,
|
||||
placeholder: Optional[str] = ...,
|
||||
custom_id: str = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
disabled: bool = ...,
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, SelectT]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def select(
|
||||
self,
|
||||
*,
|
||||
cls: Type[UserSelectT] = UserSelect[Any],
|
||||
options: List[SelectOption] = MISSING,
|
||||
channel_types: List[ChannelType] = ...,
|
||||
placeholder: Optional[str] = ...,
|
||||
custom_id: str = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
disabled: bool = ...,
|
||||
default_values: Sequence[ValidDefaultValues] = ...,
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, UserSelectT]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def select(
|
||||
self,
|
||||
*,
|
||||
cls: Type[RoleSelectT] = RoleSelect[Any],
|
||||
options: List[SelectOption] = MISSING,
|
||||
channel_types: List[ChannelType] = ...,
|
||||
placeholder: Optional[str] = ...,
|
||||
custom_id: str = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
disabled: bool = ...,
|
||||
default_values: Sequence[ValidDefaultValues] = ...,
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, RoleSelectT]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def select(
|
||||
self,
|
||||
*,
|
||||
cls: Type[ChannelSelectT] = ChannelSelect[Any],
|
||||
options: List[SelectOption] = MISSING,
|
||||
channel_types: List[ChannelType] = ...,
|
||||
placeholder: Optional[str] = ...,
|
||||
custom_id: str = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
disabled: bool = ...,
|
||||
default_values: Sequence[ValidDefaultValues] = ...,
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, ChannelSelectT]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def select(
|
||||
self,
|
||||
*,
|
||||
cls: Type[MentionableSelectT] = MentionableSelect[Any],
|
||||
options: List[SelectOption] = MISSING,
|
||||
channel_types: List[ChannelType] = MISSING,
|
||||
placeholder: Optional[str] = ...,
|
||||
custom_id: str = ...,
|
||||
min_values: int = ...,
|
||||
max_values: int = ...,
|
||||
disabled: bool = ...,
|
||||
default_values: Sequence[ValidDefaultValues] = ...,
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, MentionableSelectT]:
|
||||
...
|
||||
|
||||
def select(
|
||||
self,
|
||||
*,
|
||||
cls: Type[BaseSelectT] = Select[Any],
|
||||
options: List[SelectOption] = MISSING,
|
||||
channel_types: List[ChannelType] = MISSING,
|
||||
placeholder: Optional[str] = None,
|
||||
custom_id: str = MISSING,
|
||||
min_values: int = 1,
|
||||
max_values: int = 1,
|
||||
disabled: bool = False,
|
||||
default_values: Sequence[ValidDefaultValues] = MISSING,
|
||||
id: Optional[int] = None,
|
||||
) -> SelectCallbackDecorator[S, BaseSelectT]:
|
||||
"""A decorator that attaches a select menu to the action row.
|
||||
|
||||
The function being decorated should have three parameters, ``self`` representing
|
||||
the :class:`discord.ui.ActionRow`, the :class:`discord.Interaction` you receive and
|
||||
the chosen select class.
|
||||
|
||||
To obtain the selected values inside the callback, you can use the ``values`` attribute of the chosen class in the callback. The list of values
|
||||
will depend on the type of select menu used. View the table below for more information.
|
||||
|
||||
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| Select Type | Resolved Values |
|
||||
+========================================+=================================================================================================================+
|
||||
| :class:`discord.ui.Select` | List[:class:`str`] |
|
||||
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| :class:`discord.ui.UserSelect` | List[Union[:class:`discord.Member`, :class:`discord.User`]] |
|
||||
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| :class:`discord.ui.RoleSelect` | List[:class:`discord.Role`] |
|
||||
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| :class:`discord.ui.MentionableSelect` | List[Union[:class:`discord.Role`, :class:`discord.Member`, :class:`discord.User`]] |
|
||||
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
|
||||
| :class:`discord.ui.ChannelSelect` | List[Union[:class:`~discord.app_commands.AppCommandChannel`, :class:`~discord.app_commands.AppCommandThread`]] |
|
||||
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
Example
|
||||
---------
|
||||
.. code-block:: python3
|
||||
|
||||
class MyView(discord.ui.LayoutView):
|
||||
action_row = discord.ui.ActionRow()
|
||||
|
||||
@action_row.select(cls=ChannelSelect, channel_types=[discord.ChannelType.text])
|
||||
async def select_channels(self, interaction: discord.Interaction, select: ChannelSelect):
|
||||
return await interaction.response.send_message(f'You selected {select.values[0].mention}')
|
||||
|
||||
Parameters
|
||||
------------
|
||||
cls: Union[Type[:class:`discord.ui.Select`], Type[:class:`discord.ui.UserSelect`], Type[:class:`discord.ui.RoleSelect`], \
|
||||
Type[:class:`discord.ui.MentionableSelect`], Type[:class:`discord.ui.ChannelSelect`]]
|
||||
The class to use for the select menu. Defaults to :class:`discord.ui.Select`. You can use other
|
||||
select types to display different select menus to the user. See the table above for the different
|
||||
values you can get from each select type. Subclasses work as well, however the callback in the subclass will
|
||||
get overridden.
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text that is shown if nothing is selected, if any.
|
||||
Can only be up to 150 characters.
|
||||
custom_id: :class:`str`
|
||||
The ID of the select menu that gets received during an interaction.
|
||||
It is recommended not to set this parameter to prevent conflicts.
|
||||
Can only be up to 100 characters.
|
||||
min_values: :class:`int`
|
||||
The minimum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 0 and 25.
|
||||
max_values: :class:`int`
|
||||
The maximum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 1 and 25.
|
||||
options: List[:class:`discord.SelectOption`]
|
||||
A list of options that can be selected in this menu. This can only be used with
|
||||
:class:`Select` instances.
|
||||
Can only contain up to 25 items.
|
||||
channel_types: List[:class:`~discord.ChannelType`]
|
||||
The types of channels to show in the select menu. Defaults to all channels. This can only be used
|
||||
with :class:`ChannelSelect` instances.
|
||||
disabled: :class:`bool`
|
||||
Whether the select is disabled or not. Defaults to ``False``.
|
||||
default_values: Sequence[:class:`~discord.abc.Snowflake`]
|
||||
A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances.
|
||||
If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor.
|
||||
Number of items must be in range of ``min_values`` and ``max_values``.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
def decorator(func: ItemCallbackType[S, BaseSelectT]) -> ItemCallbackType[S, BaseSelectT]:
|
||||
r = _select( # type: ignore
|
||||
cls=cls, # type: ignore
|
||||
placeholder=placeholder,
|
||||
custom_id=custom_id,
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
options=options,
|
||||
channel_types=channel_types,
|
||||
disabled=disabled,
|
||||
default_values=default_values,
|
||||
id=id,
|
||||
)(func)
|
||||
r.__discord_ui_parent__ = self
|
||||
return r
|
||||
|
||||
return decorator # type: ignore
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: ActionRowComponent) -> ActionRow:
|
||||
from .view import _component_to_item
|
||||
|
||||
self = cls(id=component.id)
|
||||
for cmp in component.children:
|
||||
self.add_item(_component_to_item(cmp, self))
|
||||
return self
|
@ -24,12 +24,13 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Literal, Optional, TYPE_CHECKING, Tuple, TypeVar, Union
|
||||
import copy
|
||||
from typing import Any, Callable, Coroutine, Literal, Optional, TYPE_CHECKING, Tuple, TypeVar, Union
|
||||
import inspect
|
||||
import os
|
||||
|
||||
|
||||
from .item import Item, ItemCallbackType
|
||||
from .item import Item, I
|
||||
from ..enums import ButtonStyle, ComponentType
|
||||
from ..partial_emoji import PartialEmoji, _EmojiTag
|
||||
from ..components import Button as ButtonComponent
|
||||
@ -42,11 +43,16 @@ __all__ = (
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .view import View
|
||||
from .view import BaseView
|
||||
from .action_row import ActionRow
|
||||
from ..emoji import Emoji
|
||||
from ..interactions import Interaction
|
||||
from ..types.components import ButtonComponent as ButtonComponentPayload
|
||||
|
||||
V = TypeVar('V', bound='View', covariant=True)
|
||||
ItemCallbackType = Callable[['S', Interaction[Any], I], Coroutine[Any, Any, Any]]
|
||||
|
||||
S = TypeVar('S', bound='Union[BaseView, ActionRow]', covariant=True)
|
||||
V = TypeVar('V', bound='BaseView', covariant=True)
|
||||
|
||||
|
||||
class Button(Item[V]):
|
||||
@ -77,11 +83,19 @@ class Button(Item[V]):
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
|
||||
sku_id: Optional[:class:`int`]
|
||||
The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji``
|
||||
nor ``custom_id``.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: Tuple[str, ...] = (
|
||||
@ -92,6 +106,7 @@ class Button(Item[V]):
|
||||
'emoji',
|
||||
'row',
|
||||
'sku_id',
|
||||
'id',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@ -105,6 +120,7 @@ class Button(Item[V]):
|
||||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||
row: Optional[int] = None,
|
||||
sku_id: Optional[int] = None,
|
||||
id: Optional[int] = None,
|
||||
):
|
||||
super().__init__()
|
||||
if custom_id is not None and (url is not None or sku_id is not None):
|
||||
@ -143,9 +159,19 @@ class Button(Item[V]):
|
||||
style=style,
|
||||
emoji=emoji,
|
||||
sku_id=sku_id,
|
||||
id=id,
|
||||
)
|
||||
self.row = row
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[int]:
|
||||
"""Optional[:class:`int`]: The ID of this button."""
|
||||
return self._underlying.id
|
||||
|
||||
@id.setter
|
||||
def id(self, value: Optional[int]) -> None:
|
||||
self._underlying.id = value
|
||||
|
||||
@property
|
||||
def style(self) -> ButtonStyle:
|
||||
""":class:`discord.ButtonStyle`: The style of the button."""
|
||||
@ -242,6 +268,7 @@ class Button(Item[V]):
|
||||
emoji=button.emoji,
|
||||
row=None,
|
||||
sku_id=button.sku_id,
|
||||
id=button.id,
|
||||
)
|
||||
|
||||
@property
|
||||
@ -262,6 +289,28 @@ class Button(Item[V]):
|
||||
def _refresh_component(self, button: ButtonComponent) -> None:
|
||||
self._underlying = button
|
||||
|
||||
def copy(self) -> Self:
|
||||
new = copy.copy(self)
|
||||
custom_id = self.custom_id
|
||||
|
||||
if self.custom_id is not None and not self._provided_custom_id:
|
||||
custom_id = os.urandom(16).hex()
|
||||
|
||||
new._underlying = ButtonComponent._raw_construct(
|
||||
custom_id=custom_id,
|
||||
url=self.url,
|
||||
disabled=self.disabled,
|
||||
label=self.label,
|
||||
style=self.style,
|
||||
emoji=self.emoji,
|
||||
sku_id=self.sku_id,
|
||||
id=self.id,
|
||||
)
|
||||
return new
|
||||
|
||||
def __deepcopy__(self, memo) -> Self:
|
||||
return self.copy()
|
||||
|
||||
|
||||
def button(
|
||||
*,
|
||||
@ -271,7 +320,8 @@ def button(
|
||||
style: ButtonStyle = ButtonStyle.secondary,
|
||||
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
|
||||
row: Optional[int] = None,
|
||||
) -> Callable[[ItemCallbackType[V, Button[V]]], Button[V]]:
|
||||
id: Optional[int] = None,
|
||||
) -> Callable[[ItemCallbackType[S, Button[V]]], Button[V]]:
|
||||
"""A decorator that attaches a button to a component.
|
||||
|
||||
The function being decorated should have three parameters, ``self`` representing
|
||||
@ -308,9 +358,17 @@ def button(
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]:
|
||||
def decorator(func: ItemCallbackType[S, Button[V]]) -> ItemCallbackType[S, Button[V]]:
|
||||
if not inspect.iscoroutinefunction(func):
|
||||
raise TypeError('button function must be a coroutine function')
|
||||
|
||||
@ -324,6 +382,7 @@ def button(
|
||||
'emoji': emoji,
|
||||
'row': row,
|
||||
'sku_id': None,
|
||||
'id': id,
|
||||
}
|
||||
return func
|
||||
|
||||
|
369
discord/ui/container.py
Normal file
369
discord/ui/container.py
Normal file
@ -0,0 +1,369 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Generator,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from .item import Item, I
|
||||
from .view import _component_to_item, LayoutView
|
||||
from ..enums import ComponentType
|
||||
from ..utils import get as _utils_get
|
||||
from ..colour import Colour, Color
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..components import Container as ContainerComponent
|
||||
from ..interactions import Interaction
|
||||
|
||||
ItemCallbackType = Callable[['S', Interaction[Any], I], Coroutine[Any, Any, Any]]
|
||||
|
||||
S = TypeVar('S', bound='Container', covariant=True)
|
||||
V = TypeVar('V', bound='LayoutView', covariant=True)
|
||||
|
||||
__all__ = ('Container',)
|
||||
|
||||
|
||||
class _ContainerCallback:
|
||||
__slots__ = ('container', 'callback', 'item')
|
||||
|
||||
def __init__(self, callback: ItemCallbackType[S, Any], container: Container, item: Item[Any]) -> None:
|
||||
self.callback: ItemCallbackType[Any, Any] = callback
|
||||
self.container: Container = container
|
||||
self.item: Item[Any] = item
|
||||
|
||||
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
|
||||
return self.callback(self.container, interaction, self.item)
|
||||
|
||||
|
||||
class Container(Item[V]):
|
||||
r"""Represents a UI container.
|
||||
|
||||
This is a top-level layout component that can only be used on :class:`LayoutView`
|
||||
and can contain :class:`ActionRow`\s, :class:`TextDisplay`\s, :class:`Section`\s,
|
||||
:class:`MediaGallery`\s, :class:`File`\s, and :class:`Separator`\s in it.
|
||||
|
||||
This can be inherited.
|
||||
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
import discord
|
||||
from discord import ui
|
||||
|
||||
# you can subclass it and add components as you would add them
|
||||
# in a LayoutView
|
||||
class MyContainer(ui.Container):
|
||||
action_row = ui.ActionRow()
|
||||
|
||||
@action_row.button(label='A button in a container!')
|
||||
async def a_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.send_message('You clicked a button!')
|
||||
|
||||
# or use it directly on LayoutView
|
||||
class MyView(ui.LayoutView):
|
||||
container = ui.Container(ui.TextDisplay('I am a text display on a container!'))
|
||||
# or you can use your subclass:
|
||||
# container = MyContainer()
|
||||
|
||||
Parameters
|
||||
----------
|
||||
\*children: :class:`Item`
|
||||
The initial children of this container.
|
||||
accent_colour: Optional[Union[:class:`.Colour`, :class:`int`]]
|
||||
The colour of the container. Defaults to ``None``.
|
||||
accent_color: Optional[Union[:class:`.Colour`, :class:`int`]]
|
||||
The color of the container. Defaults to ``None``.
|
||||
spoiler: :class:`bool`
|
||||
Whether to flag this container as a spoiler. Defaults
|
||||
to ``False``.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
"""
|
||||
|
||||
__container_children_items__: ClassVar[Dict[str, Union[ItemCallbackType[Self, Any], Item[Any]]]] = {}
|
||||
__discord_ui_container__: ClassVar[bool] = True
|
||||
__item_repr_attributes__ = (
|
||||
'accent_colour',
|
||||
'spoiler',
|
||||
'id',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: Item[V],
|
||||
accent_colour: Optional[Union[Colour, int]] = None,
|
||||
accent_color: Optional[Union[Color, int]] = None,
|
||||
spoiler: bool = False,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._children: List[Item[V]] = self._init_children()
|
||||
for child in children:
|
||||
self.add_item(child)
|
||||
|
||||
self.spoiler: bool = spoiler
|
||||
self._colour = accent_colour if accent_colour is not None else accent_color
|
||||
self.id = id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} children={len(self._children)}>'
|
||||
|
||||
def _init_children(self) -> List[Item[Any]]:
|
||||
children = []
|
||||
parents = {}
|
||||
|
||||
for name, raw in self.__container_children_items__.items():
|
||||
if isinstance(raw, Item):
|
||||
item = raw.copy()
|
||||
item._parent = self
|
||||
setattr(self, name, item)
|
||||
children.append(item)
|
||||
parents[raw] = item
|
||||
else:
|
||||
# action rows can be created inside containers, and then callbacks can exist here
|
||||
# so we create items based off them
|
||||
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
|
||||
item.callback = _ContainerCallback(raw, self, item) # type: ignore
|
||||
setattr(self, raw.__name__, item)
|
||||
# this should not fail because in order for a function to be here it should be from
|
||||
# an action row and must have passed the check in __init_subclass__, but still
|
||||
# guarding it
|
||||
parent = getattr(raw, '__discord_ui_parent__', None)
|
||||
if parent is None:
|
||||
raise ValueError(f'{raw.__name__} is not a valid item for a Container')
|
||||
parents.get(parent, parent)._children.append(item)
|
||||
# we do not append it to the children list because technically these buttons and
|
||||
# selects are not from the container but the action row itself.
|
||||
|
||||
return children
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
|
||||
children: Dict[str, Union[ItemCallbackType[Self, Any], Item[Any]]] = {}
|
||||
for base in reversed(cls.__mro__):
|
||||
for name, member in base.__dict__.items():
|
||||
if isinstance(member, Item):
|
||||
children[name] = member
|
||||
if hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None):
|
||||
children[name] = copy.copy(member)
|
||||
|
||||
cls.__container_children_items__ = children
|
||||
|
||||
def _update_view(self, view) -> bool:
|
||||
self._view = view
|
||||
for child in self._children:
|
||||
child._update_view(view)
|
||||
return True
|
||||
|
||||
def _has_children(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def children(self) -> List[Item[V]]:
|
||||
"""List[:class:`Item`]: The children of this container."""
|
||||
return self._children.copy()
|
||||
|
||||
@children.setter
|
||||
def children(self, value: List[Item[V]]) -> None:
|
||||
self._children = value
|
||||
|
||||
@property
|
||||
def accent_colour(self) -> Optional[Union[Colour, int]]:
|
||||
"""Optional[Union[:class:`discord.Colour`, :class:`int`]]: The colour of the container, or ``None``."""
|
||||
return self._colour
|
||||
|
||||
@accent_colour.setter
|
||||
def accent_colour(self, value: Optional[Union[Colour, int]]) -> None:
|
||||
if value is not None and not isinstance(value, (int, Colour)):
|
||||
raise TypeError(f'expected an int, or Colour, not {value.__class__.__name__!r}')
|
||||
|
||||
self._colour = value
|
||||
|
||||
accent_color = accent_colour
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.container]:
|
||||
return ComponentType.container
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return 5
|
||||
|
||||
def _is_v2(self) -> bool:
|
||||
return True
|
||||
|
||||
def to_components(self) -> List[Dict[str, Any]]:
|
||||
components = []
|
||||
for i in self._children:
|
||||
components.append(i.to_component_dict())
|
||||
return components
|
||||
|
||||
def to_component_dict(self) -> Dict[str, Any]:
|
||||
components = self.to_components()
|
||||
|
||||
colour = None
|
||||
if self._colour:
|
||||
colour = self._colour if isinstance(self._colour, int) else self._colour.value
|
||||
|
||||
base = {
|
||||
'type': self.type.value,
|
||||
'accent_color': colour,
|
||||
'spoiler': self.spoiler,
|
||||
'components': components,
|
||||
}
|
||||
if self.id is not None:
|
||||
base['id'] = self.id
|
||||
return base
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: ContainerComponent) -> Self:
|
||||
self = cls(
|
||||
accent_colour=component.accent_colour,
|
||||
spoiler=component.spoiler,
|
||||
id=component.id,
|
||||
)
|
||||
self._children = [_component_to_item(cmp, self) for cmp in component.children]
|
||||
return self
|
||||
|
||||
def walk_children(self) -> Generator[Item[V], None, None]:
|
||||
"""An iterator that recursively walks through all the children of this container
|
||||
and its children, if applicable.
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`Item`
|
||||
An item in the container.
|
||||
"""
|
||||
|
||||
for child in self.children:
|
||||
yield child
|
||||
|
||||
if child._has_children():
|
||||
yield from child.walk_children() # type: ignore
|
||||
|
||||
def add_item(self, item: Item[Any]) -> Self:
|
||||
"""Adds an item to this container.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`Item`
|
||||
The item to append.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
An :class:`Item` was not passed.
|
||||
"""
|
||||
if not isinstance(item, Item):
|
||||
raise TypeError(f'expected Item not {item.__class__.__name__}')
|
||||
|
||||
self._children.append(item)
|
||||
item._update_view(self.view)
|
||||
item._parent = self
|
||||
|
||||
if item._has_children() and self._view:
|
||||
self._view._total_children += len(tuple(item.walk_children())) # type: ignore
|
||||
elif self._view:
|
||||
self._view._total_children += 1
|
||||
return self
|
||||
|
||||
def remove_item(self, item: Item[Any]) -> Self:
|
||||
"""Removes an item from this container.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`Item`
|
||||
The item to remove from the container.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._children.remove(item)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if self._view and self._view._is_layout():
|
||||
if item._has_children():
|
||||
self._view._total_children -= len(tuple(item.walk_children())) # type: ignore
|
||||
else:
|
||||
self._view._total_children -= 1
|
||||
return self
|
||||
|
||||
def find_item(self, id: int, /) -> Optional[Item[V]]:
|
||||
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
|
||||
not found.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is **not the same** as ``custom_id``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id: :class:`int`
|
||||
The ID of the component.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`Item`]
|
||||
The item found, or ``None``.
|
||||
"""
|
||||
return _utils_get(self.walk_children(), id=id)
|
||||
|
||||
def clear_items(self) -> Self:
|
||||
"""Removes all the items from the container.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
"""
|
||||
|
||||
if self._view and self._view._is_layout():
|
||||
self._view._total_children -= len(tuple(self.walk_children()))
|
||||
self._children.clear()
|
||||
return self
|
@ -38,14 +38,12 @@ if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from ..components import Component
|
||||
from ..enums import ComponentType
|
||||
from .view import View
|
||||
|
||||
V = TypeVar('V', bound='View', covariant=True, default=View)
|
||||
from .view import View, LayoutView
|
||||
else:
|
||||
V = TypeVar('V', bound='View', covariant=True)
|
||||
View = LayoutView = Any
|
||||
|
||||
|
||||
class DynamicItem(Generic[BaseT], Item['View']):
|
||||
class DynamicItem(Generic[BaseT], Item[Union[View, LayoutView]]):
|
||||
"""Represents an item with a dynamic ``custom_id`` that can be used to store state within
|
||||
that ``custom_id``.
|
||||
|
||||
@ -57,9 +55,10 @@ class DynamicItem(Generic[BaseT], Item['View']):
|
||||
and should not be used long term. Their only purpose is to act as a "template"
|
||||
for the actual dispatched item.
|
||||
|
||||
When this item is generated, :attr:`view` is set to a regular :class:`View` instance
|
||||
from the original message given from the interaction. This means that custom view
|
||||
subclasses cannot be accessed from this item.
|
||||
When this item is generated, :attr:`view` is set to a regular :class:`View` instance,
|
||||
but to a :class:`LayoutView` if the component was sent with one, this is obtained from
|
||||
the original message given from the interaction. This means that custom view subclasses
|
||||
cannot be accessed from this item.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
|
||||
|
146
discord/ui/file.py
Normal file
146
discord/ui/file.py
Normal file
@ -0,0 +1,146 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union
|
||||
|
||||
from .item import Item
|
||||
from ..components import FileComponent, UnfurledMediaItem
|
||||
from ..enums import ComponentType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .view import LayoutView
|
||||
|
||||
V = TypeVar('V', bound='LayoutView', covariant=True)
|
||||
|
||||
__all__ = ('File',)
|
||||
|
||||
|
||||
class File(Item[V]):
|
||||
"""Represents a UI file component.
|
||||
|
||||
This is a top-level layout component that can only be used on :class:`LayoutView`.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
import discord
|
||||
from discord import ui
|
||||
|
||||
class MyView(ui.LayoutView):
|
||||
file = ui.File('attachment://file.txt')
|
||||
# attachment://file.txt points to an attachment uploaded alongside this view
|
||||
|
||||
Parameters
|
||||
----------
|
||||
media: Union[:class:`str`, :class:`.UnfurledMediaItem`]
|
||||
This file's media. If this is a string it must point to a local
|
||||
file uploaded within the parent view of this item, and must
|
||||
meet the ``attachment://<filename>`` format.
|
||||
spoiler: :class:`bool`
|
||||
Whether to flag this file as a spoiler. Defaults to ``False``.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__ = (
|
||||
'media',
|
||||
'spoiler',
|
||||
'id',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
media: Union[str, UnfurledMediaItem],
|
||||
*,
|
||||
spoiler: bool = False,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._underlying = FileComponent._raw_construct(
|
||||
media=UnfurledMediaItem(media) if isinstance(media, str) else media,
|
||||
spoiler=spoiler,
|
||||
id=id,
|
||||
)
|
||||
self.id = id
|
||||
|
||||
def _is_v2(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return 5
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.file]:
|
||||
return self._underlying.type
|
||||
|
||||
@property
|
||||
def media(self) -> UnfurledMediaItem:
|
||||
""":class:`.UnfurledMediaItem`: Returns this file media."""
|
||||
return self._underlying.media
|
||||
|
||||
@media.setter
|
||||
def media(self, value: Union[str, UnfurledMediaItem]) -> None:
|
||||
if isinstance(value, str):
|
||||
self._underlying.media = UnfurledMediaItem(value)
|
||||
elif isinstance(value, UnfurledMediaItem):
|
||||
self._underlying.media = value
|
||||
else:
|
||||
raise TypeError(f'expected a str or UnfurledMediaItem, not {value.__class__.__name__!r}')
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
""":class:`str`: Returns this file's url."""
|
||||
return self._underlying.media.url
|
||||
|
||||
@url.setter
|
||||
def url(self, value: str) -> None:
|
||||
self._underlying.media = UnfurledMediaItem(value)
|
||||
|
||||
@property
|
||||
def spoiler(self) -> bool:
|
||||
""":class:`bool`: Returns whether this file should be flagged as a spoiler."""
|
||||
return self._underlying.spoiler
|
||||
|
||||
@spoiler.setter
|
||||
def spoiler(self, value: bool) -> None:
|
||||
self._underlying.spoiler = value
|
||||
|
||||
def to_component_dict(self):
|
||||
return self._underlying.to_dict()
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: FileComponent) -> Self:
|
||||
return cls(
|
||||
media=component.media,
|
||||
spoiler=component.spoiler,
|
||||
id=component.id,
|
||||
)
|
@ -24,6 +24,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Tuple, Type, TypeVar
|
||||
|
||||
from ..interactions import Interaction
|
||||
@ -36,12 +37,14 @@ __all__ = (
|
||||
# fmt: on
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..enums import ComponentType
|
||||
from .view import View
|
||||
from .view import BaseView
|
||||
from ..components import Component
|
||||
|
||||
I = TypeVar('I', bound='Item[Any]')
|
||||
V = TypeVar('V', bound='View', covariant=True)
|
||||
V = TypeVar('V', bound='BaseView', covariant=True)
|
||||
ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]]
|
||||
|
||||
|
||||
@ -53,11 +56,19 @@ class Item(Generic[V]):
|
||||
- :class:`discord.ui.Button`
|
||||
- :class:`discord.ui.Select`
|
||||
- :class:`discord.ui.TextInput`
|
||||
- :class:`discord.ui.ActionRow`
|
||||
- :class:`discord.ui.Container`
|
||||
- :class:`discord.ui.File`
|
||||
- :class:`discord.ui.MediaGallery`
|
||||
- :class:`discord.ui.Section`
|
||||
- :class:`discord.ui.Separator`
|
||||
- :class:`discord.ui.TextDisplay`
|
||||
- :class:`discord.ui.Thumbnail`
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: Tuple[str, ...] = ('row',)
|
||||
__item_repr_attributes__: Tuple[str, ...] = ('row', 'id')
|
||||
|
||||
def __init__(self):
|
||||
self._view: Optional[V] = None
|
||||
@ -70,6 +81,8 @@ class Item(Generic[V]):
|
||||
# actually affect the intended purpose of this check because from_component is
|
||||
# only called upon edit and we're mainly interested during initial creation time.
|
||||
self._provided_custom_id: bool = False
|
||||
self._id: Optional[int] = None
|
||||
self._parent: Optional[Item] = None
|
||||
|
||||
def to_component_dict(self) -> Dict[str, Any]:
|
||||
raise NotImplementedError
|
||||
@ -80,6 +93,9 @@ class Item(Generic[V]):
|
||||
def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None:
|
||||
return None
|
||||
|
||||
def _is_v2(self) -> bool:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: Type[I], component: Component) -> I:
|
||||
return cls()
|
||||
@ -92,7 +108,9 @@ class Item(Generic[V]):
|
||||
return False
|
||||
|
||||
def is_persistent(self) -> bool:
|
||||
return self._provided_custom_id
|
||||
if self.is_dispatchable():
|
||||
return self._provided_custom_id
|
||||
return True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__)
|
||||
@ -104,6 +122,10 @@ class Item(Generic[V]):
|
||||
|
||||
@row.setter
|
||||
def row(self, value: Optional[int]) -> None:
|
||||
if self._is_v2():
|
||||
# row is ignored on v2 components
|
||||
return
|
||||
|
||||
if value is None:
|
||||
self._row = None
|
||||
elif 5 > value >= 0:
|
||||
@ -117,9 +139,45 @@ class Item(Generic[V]):
|
||||
|
||||
@property
|
||||
def view(self) -> Optional[V]:
|
||||
"""Optional[:class:`View`]: The underlying view for this item."""
|
||||
"""Optional[Union[:class:`View`, :class:`LayoutView`]]: The underlying view for this item."""
|
||||
return self._view
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[int]:
|
||||
"""Optional[:class:`int`]: The ID of this component."""
|
||||
return self._id
|
||||
|
||||
@id.setter
|
||||
def id(self, value: Optional[int]) -> None:
|
||||
self._id = value
|
||||
|
||||
@property
|
||||
def parent(self) -> Optional[Item[V]]:
|
||||
"""Optional[:class:`Item`]: This item's parent. Only components that can have children
|
||||
can be parents. Any item that has :class:`View` as a view will have this set to `None`
|
||||
since only :class:`LayoutView` component v2 items can contain "container" like items.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
return self._parent
|
||||
|
||||
async def _run_checks(self, interaction: Interaction[ClientT]) -> bool:
|
||||
can_run = await self.interaction_check(interaction)
|
||||
|
||||
if can_run and self._parent:
|
||||
can_run = await self._parent._run_checks(interaction)
|
||||
|
||||
return can_run
|
||||
|
||||
def _update_view(self, view) -> None:
|
||||
self._view = view
|
||||
|
||||
def copy(self) -> Self:
|
||||
return copy.deepcopy(self)
|
||||
|
||||
def _has_children(self) -> bool:
|
||||
return False
|
||||
|
||||
async def callback(self, interaction: Interaction[ClientT]) -> Any:
|
||||
"""|coro|
|
||||
|
||||
|
260
discord/ui/media_gallery.py
Normal file
260
discord/ui/media_gallery.py
Normal file
@ -0,0 +1,260 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, List, Literal, Optional, TypeVar, Union
|
||||
|
||||
from .item import Item
|
||||
from ..enums import ComponentType
|
||||
from ..components import (
|
||||
MediaGalleryItem,
|
||||
MediaGalleryComponent,
|
||||
UnfurledMediaItem,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .view import LayoutView
|
||||
|
||||
V = TypeVar('V', bound='LayoutView', covariant=True)
|
||||
|
||||
__all__ = ('MediaGallery',)
|
||||
|
||||
|
||||
class MediaGallery(Item[V]):
|
||||
r"""Represents a UI media gallery.
|
||||
|
||||
Can contain up to 10 :class:`.MediaGalleryItem`\s.
|
||||
|
||||
This is a top-level layout component that can only be used on :class:`LayoutView`.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
\*items: :class:`.MediaGalleryItem`
|
||||
The initial items of this gallery.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__ = (
|
||||
'items',
|
||||
'id',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*items: MediaGalleryItem,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._underlying = MediaGalleryComponent._raw_construct(
|
||||
items=list(items),
|
||||
id=id,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} items={len(self._underlying.items)}>'
|
||||
|
||||
@property
|
||||
def items(self) -> List[MediaGalleryItem]:
|
||||
"""List[:class:`.MediaGalleryItem`]: Returns a read-only list of this gallery's items."""
|
||||
return self._underlying.items.copy()
|
||||
|
||||
@items.setter
|
||||
def items(self, value: List[MediaGalleryItem]) -> None:
|
||||
if len(value) > 10:
|
||||
raise ValueError('media gallery only accepts up to 10 items')
|
||||
|
||||
self._underlying.items = value
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[int]:
|
||||
"""Optional[:class:`int`]: The ID of this component."""
|
||||
return self._underlying.id
|
||||
|
||||
@id.setter
|
||||
def id(self, value: Optional[int]) -> None:
|
||||
self._underlying.id = value
|
||||
|
||||
def to_component_dict(self):
|
||||
return self._underlying.to_dict()
|
||||
|
||||
def _is_v2(self) -> bool:
|
||||
return True
|
||||
|
||||
def add_item(
|
||||
self,
|
||||
*,
|
||||
media: Union[str, UnfurledMediaItem],
|
||||
description: Optional[str] = None,
|
||||
spoiler: bool = False,
|
||||
) -> Self:
|
||||
"""Adds an item to this gallery.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
media: Union[:class:`str`, :class:`.UnfurledMediaItem`]
|
||||
The media item data. This can be a string representing a local
|
||||
file uploaded as an attachment in the message, which can be accessed
|
||||
using the ``attachment://<filename>`` format, or an arbitrary url.
|
||||
description: Optional[:class:`str`]
|
||||
The description to show within this item. Up to 256 characters. Defaults
|
||||
to ``None``.
|
||||
spoiler: :class:`bool`
|
||||
Whether this item should be flagged as a spoiler. Defaults to ``False``.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
Maximum number of items has been exceeded (10).
|
||||
"""
|
||||
|
||||
if len(self._underlying.items) >= 10:
|
||||
raise ValueError('maximum number of items has been exceeded')
|
||||
|
||||
item = MediaGalleryItem(media, description=description, spoiler=spoiler)
|
||||
self._underlying.items.append(item)
|
||||
return self
|
||||
|
||||
def append_item(self, item: MediaGalleryItem) -> Self:
|
||||
"""Appends an item to this gallery.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`.MediaGalleryItem`
|
||||
The item to add to the gallery.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
A :class:`.MediaGalleryItem` was not passed.
|
||||
ValueError
|
||||
Maximum number of items has been exceeded (10).
|
||||
"""
|
||||
|
||||
if len(self._underlying.items) >= 10:
|
||||
raise ValueError('maximum number of items has been exceeded')
|
||||
|
||||
if not isinstance(item, MediaGalleryItem):
|
||||
raise TypeError(f'expected MediaGalleryItem, not {item.__class__.__name__!r}')
|
||||
|
||||
self._underlying.items.append(item)
|
||||
return self
|
||||
|
||||
def insert_item_at(
|
||||
self,
|
||||
index: int,
|
||||
*,
|
||||
media: Union[str, UnfurledMediaItem],
|
||||
description: Optional[str] = None,
|
||||
spoiler: bool = False,
|
||||
) -> Self:
|
||||
"""Inserts an item before a specified index to the media gallery.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
index: :class:`int`
|
||||
The index of where to insert the field.
|
||||
media: Union[:class:`str`, :class:`.UnfurledMediaItem`]
|
||||
The media item data. This can be a string representing a local
|
||||
file uploaded as an attachment in the message, which can be accessed
|
||||
using the ``attachment://<filename>`` format, or an arbitrary url.
|
||||
description: Optional[:class:`str`]
|
||||
The description to show within this item. Up to 256 characters. Defaults
|
||||
to ``None``.
|
||||
spoiler: :class:`bool`
|
||||
Whether this item should be flagged as a spoiler. Defaults to ``False``.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
Maximum number of items has been exceeded (10).
|
||||
"""
|
||||
|
||||
if len(self._underlying.items) >= 10:
|
||||
raise ValueError('maximum number of items has been exceeded')
|
||||
|
||||
item = MediaGalleryItem(
|
||||
media,
|
||||
description=description,
|
||||
spoiler=spoiler,
|
||||
)
|
||||
self._underlying.items.insert(index, item)
|
||||
return self
|
||||
|
||||
def remove_item(self, item: MediaGalleryItem) -> Self:
|
||||
"""Removes an item from the gallery.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`.MediaGalleryItem`
|
||||
The item to remove from the gallery.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._underlying.items.remove(item)
|
||||
except ValueError:
|
||||
pass
|
||||
return self
|
||||
|
||||
def clear_items(self) -> Self:
|
||||
"""Removes all items from the gallery.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
"""
|
||||
|
||||
self._underlying.items.clear()
|
||||
return self
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.media_gallery]:
|
||||
return self._underlying.type
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return 5
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: MediaGalleryComponent) -> Self:
|
||||
return cls(
|
||||
*component.items,
|
||||
id=component.id,
|
||||
)
|
248
discord/ui/section.py
Normal file
248
discord/ui/section.py
Normal file
@ -0,0 +1,248 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Literal, Optional, TypeVar, Union, ClassVar
|
||||
|
||||
from .item import Item
|
||||
from .text_display import TextDisplay
|
||||
from ..enums import ComponentType
|
||||
from ..utils import MISSING, get as _utils_get
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .view import LayoutView
|
||||
from ..components import SectionComponent
|
||||
|
||||
V = TypeVar('V', bound='LayoutView', covariant=True)
|
||||
|
||||
__all__ = ('Section',)
|
||||
|
||||
|
||||
class Section(Item[V]):
|
||||
r"""Represents a UI section.
|
||||
|
||||
This is a top-level layout component that can only be used on :class:`LayoutView`.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
\*children: Union[:class:`str`, :class:`TextDisplay`]
|
||||
The text displays of this section. Up to 3.
|
||||
accessory: :class:`Item`
|
||||
The section accessory.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__ = (
|
||||
'accessory',
|
||||
'id',
|
||||
)
|
||||
__discord_ui_section__: ClassVar[bool] = True
|
||||
|
||||
__slots__ = (
|
||||
'_children',
|
||||
'accessory',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: Union[Item[V], str],
|
||||
accessory: Item[V],
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._children: List[Item[V]] = []
|
||||
if children:
|
||||
if len(children) > 3:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
self._children.extend(
|
||||
[c if isinstance(c, Item) else TextDisplay(c) for c in children],
|
||||
)
|
||||
self.accessory: Item[V] = accessory
|
||||
self.id = id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} children={len(self._children)}>'
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.section]:
|
||||
return ComponentType.section
|
||||
|
||||
@property
|
||||
def children(self) -> List[Item[V]]:
|
||||
"""List[:class:`Item`]: The list of children attached to this section."""
|
||||
return self._children.copy()
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return 5
|
||||
|
||||
def _is_v2(self) -> bool:
|
||||
return True
|
||||
|
||||
def walk_children(self) -> Generator[Item[V], None, None]:
|
||||
"""An iterator that recursively walks through all the children of this section
|
||||
and its children, if applicable. This includes the `accessory`.
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`Item`
|
||||
An item in this section.
|
||||
"""
|
||||
|
||||
for child in self.children:
|
||||
yield child
|
||||
yield self.accessory
|
||||
|
||||
def _update_view(self, view) -> None:
|
||||
self._view = view
|
||||
self.accessory._view = view
|
||||
for child in self._children:
|
||||
child._view = view
|
||||
|
||||
def _has_children(self):
|
||||
return True
|
||||
|
||||
def add_item(self, item: Union[str, Item[Any]]) -> Self:
|
||||
"""Adds an item to this section.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: Union[:class:`str`, :class:`Item`]
|
||||
The item to append, if it is a string it automatically wrapped around
|
||||
:class:`TextDisplay`.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
An :class:`Item` or :class:`str` was not passed.
|
||||
ValueError
|
||||
Maximum number of children has been exceeded (3).
|
||||
"""
|
||||
|
||||
if len(self._children) >= 3:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
|
||||
if not isinstance(item, (Item, str)):
|
||||
raise TypeError(f'expected Item or str not {item.__class__.__name__}')
|
||||
|
||||
item = item if isinstance(item, Item) else TextDisplay(item)
|
||||
item._update_view(self.view)
|
||||
item._parent = self
|
||||
self._children.append(item)
|
||||
|
||||
if self._view:
|
||||
self._view._total_children += 1
|
||||
|
||||
return self
|
||||
|
||||
def remove_item(self, item: Item[Any]) -> Self:
|
||||
"""Removes an item from this section.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
item: :class:`Item`
|
||||
The item to remove from the section.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._children.remove(item)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if self._view:
|
||||
self._view._total_children -= 1
|
||||
|
||||
return self
|
||||
|
||||
def find_item(self, id: int, /) -> Optional[Item[V]]:
|
||||
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
|
||||
not found.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is **not the same** as ``custom_id``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id: :class:`int`
|
||||
The ID of the component.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`Item`]
|
||||
The item found, or ``None``.
|
||||
"""
|
||||
return _utils_get(self.walk_children(), id=id)
|
||||
|
||||
def clear_items(self) -> Self:
|
||||
"""Removes all the items from the section.
|
||||
|
||||
This function returns the class instance to allow for fluent-style
|
||||
chaining.
|
||||
"""
|
||||
if self._view and self._view._is_layout():
|
||||
self._view._total_children -= len(self._children) # we don't count the accessory because it is required
|
||||
|
||||
self._children.clear()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: SectionComponent) -> Self:
|
||||
from .view import _component_to_item
|
||||
|
||||
# using MISSING as accessory so we can create the new one with the parent set
|
||||
self = cls(id=component.id, accessory=MISSING)
|
||||
self.accessory = _component_to_item(component.accessory, self)
|
||||
self.id = component.id
|
||||
self._children = [_component_to_item(c, self) for c in component.children]
|
||||
|
||||
return self
|
||||
|
||||
def to_components(self) -> List[Dict[str, Any]]:
|
||||
components = []
|
||||
|
||||
for component in self._children:
|
||||
components.append(component.to_component_dict())
|
||||
return components
|
||||
|
||||
def to_component_dict(self) -> Dict[str, Any]:
|
||||
data = {
|
||||
'type': self.type.value,
|
||||
'components': self.to_components(),
|
||||
'accessory': self.accessory.to_component_dict(),
|
||||
}
|
||||
if self.id is not None:
|
||||
data['id'] = self.id
|
||||
return data
|
@ -25,6 +25,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Any,
|
||||
Coroutine,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
@ -42,7 +43,7 @@ from contextvars import ContextVar
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from .item import Item, ItemCallbackType
|
||||
from .item import Item, I
|
||||
from ..enums import ChannelType, ComponentType, SelectDefaultValueType
|
||||
from ..partial_emoji import PartialEmoji
|
||||
from ..emoji import Emoji
|
||||
@ -72,7 +73,8 @@ __all__ = (
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias, TypeGuard
|
||||
|
||||
from .view import View
|
||||
from .view import BaseView
|
||||
from .action_row import ActionRow
|
||||
from ..types.components import SelectMenu as SelectMenuPayload
|
||||
from ..types.interactions import SelectMessageComponentInteractionData
|
||||
from ..app_commands import AppCommandChannel, AppCommandThread
|
||||
@ -101,14 +103,17 @@ if TYPE_CHECKING:
|
||||
Thread,
|
||||
]
|
||||
|
||||
V = TypeVar('V', bound='View', covariant=True)
|
||||
ItemCallbackType = Callable[['S', Interaction[Any], I], Coroutine[Any, Any, Any]]
|
||||
|
||||
S = TypeVar('S', bound='Union[BaseView, ActionRow]', covariant=True)
|
||||
V = TypeVar('V', bound='BaseView', covariant=True)
|
||||
BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]')
|
||||
SelectT = TypeVar('SelectT', bound='Select[Any]')
|
||||
UserSelectT = TypeVar('UserSelectT', bound='UserSelect[Any]')
|
||||
RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect[Any]')
|
||||
ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect[Any]')
|
||||
MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect[Any]')
|
||||
SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[V, BaseSelectT]], BaseSelectT]
|
||||
SelectCallbackDecorator: TypeAlias = Callable[['ItemCallbackType[S, BaseSelectT]'], BaseSelectT]
|
||||
DefaultSelectComponentTypes = Literal[
|
||||
ComponentType.user_select,
|
||||
ComponentType.role_select,
|
||||
@ -216,6 +221,7 @@ class BaseSelect(Item[V]):
|
||||
'min_values',
|
||||
'max_values',
|
||||
'disabled',
|
||||
'id',
|
||||
)
|
||||
__component_attributes__: Tuple[str, ...] = (
|
||||
'custom_id',
|
||||
@ -223,6 +229,7 @@ class BaseSelect(Item[V]):
|
||||
'min_values',
|
||||
'max_values',
|
||||
'disabled',
|
||||
'id',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@ -238,6 +245,7 @@ class BaseSelect(Item[V]):
|
||||
options: List[SelectOption] = MISSING,
|
||||
channel_types: List[ChannelType] = MISSING,
|
||||
default_values: Sequence[SelectDefaultValue] = MISSING,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._provided_custom_id = custom_id is not MISSING
|
||||
@ -255,11 +263,21 @@ class BaseSelect(Item[V]):
|
||||
channel_types=[] if channel_types is MISSING else channel_types,
|
||||
options=[] if options is MISSING else options,
|
||||
default_values=[] if default_values is MISSING else default_values,
|
||||
id=id,
|
||||
)
|
||||
|
||||
self.row = row
|
||||
self._values: List[PossibleValue] = []
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[int]:
|
||||
"""Optional[:class:`int`]: The ID of this select."""
|
||||
return self._underlying.id
|
||||
|
||||
@id.setter
|
||||
def id(self, value: Optional[int]) -> None:
|
||||
self._underlying.id = value
|
||||
|
||||
@property
|
||||
def values(self) -> List[PossibleValue]:
|
||||
values = selected_values.get({})
|
||||
@ -390,6 +408,14 @@ class Select(BaseSelect[V]):
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__component_attributes__ = BaseSelect.__component_attributes__ + ('options',)
|
||||
@ -404,6 +430,7 @@ class Select(BaseSelect[V]):
|
||||
options: List[SelectOption] = MISSING,
|
||||
disabled: bool = False,
|
||||
row: Optional[int] = None,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
self.type,
|
||||
@ -414,6 +441,7 @@ class Select(BaseSelect[V]):
|
||||
disabled=disabled,
|
||||
options=options,
|
||||
row=row,
|
||||
id=id,
|
||||
)
|
||||
|
||||
@property
|
||||
@ -545,6 +573,14 @@ class UserSelect(BaseSelect[V]):
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
|
||||
@ -559,6 +595,7 @@ class UserSelect(BaseSelect[V]):
|
||||
disabled: bool = False,
|
||||
row: Optional[int] = None,
|
||||
default_values: Sequence[ValidDefaultValues] = MISSING,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
self.type,
|
||||
@ -569,6 +606,7 @@ class UserSelect(BaseSelect[V]):
|
||||
disabled=disabled,
|
||||
row=row,
|
||||
default_values=_handle_select_defaults(default_values, self.type),
|
||||
id=id,
|
||||
)
|
||||
|
||||
@property
|
||||
@ -637,6 +675,14 @@ class RoleSelect(BaseSelect[V]):
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
|
||||
@ -651,6 +697,7 @@ class RoleSelect(BaseSelect[V]):
|
||||
disabled: bool = False,
|
||||
row: Optional[int] = None,
|
||||
default_values: Sequence[ValidDefaultValues] = MISSING,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
self.type,
|
||||
@ -661,6 +708,7 @@ class RoleSelect(BaseSelect[V]):
|
||||
disabled=disabled,
|
||||
row=row,
|
||||
default_values=_handle_select_defaults(default_values, self.type),
|
||||
id=id,
|
||||
)
|
||||
|
||||
@property
|
||||
@ -725,6 +773,14 @@ class MentionableSelect(BaseSelect[V]):
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',)
|
||||
@ -739,6 +795,7 @@ class MentionableSelect(BaseSelect[V]):
|
||||
disabled: bool = False,
|
||||
row: Optional[int] = None,
|
||||
default_values: Sequence[ValidDefaultValues] = MISSING,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
self.type,
|
||||
@ -749,6 +806,7 @@ class MentionableSelect(BaseSelect[V]):
|
||||
disabled=disabled,
|
||||
row=row,
|
||||
default_values=_handle_select_defaults(default_values, self.type),
|
||||
id=id,
|
||||
)
|
||||
|
||||
@property
|
||||
@ -819,6 +877,14 @@ class ChannelSelect(BaseSelect[V]):
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__component_attributes__ = BaseSelect.__component_attributes__ + (
|
||||
@ -837,6 +903,7 @@ class ChannelSelect(BaseSelect[V]):
|
||||
disabled: bool = False,
|
||||
row: Optional[int] = None,
|
||||
default_values: Sequence[ValidDefaultValues] = MISSING,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
self.type,
|
||||
@ -848,6 +915,7 @@ class ChannelSelect(BaseSelect[V]):
|
||||
row=row,
|
||||
channel_types=channel_types,
|
||||
default_values=_handle_select_defaults(default_values, self.type),
|
||||
id=id,
|
||||
)
|
||||
|
||||
@property
|
||||
@ -899,7 +967,8 @@ def select(
|
||||
max_values: int = ...,
|
||||
disabled: bool = ...,
|
||||
row: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[V, SelectT]:
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, SelectT]:
|
||||
...
|
||||
|
||||
|
||||
@ -916,7 +985,8 @@ def select(
|
||||
disabled: bool = ...,
|
||||
default_values: Sequence[ValidDefaultValues] = ...,
|
||||
row: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[V, UserSelectT]:
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, UserSelectT]:
|
||||
...
|
||||
|
||||
|
||||
@ -933,7 +1003,8 @@ def select(
|
||||
disabled: bool = ...,
|
||||
default_values: Sequence[ValidDefaultValues] = ...,
|
||||
row: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[V, RoleSelectT]:
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, RoleSelectT]:
|
||||
...
|
||||
|
||||
|
||||
@ -950,7 +1021,8 @@ def select(
|
||||
disabled: bool = ...,
|
||||
default_values: Sequence[ValidDefaultValues] = ...,
|
||||
row: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[V, ChannelSelectT]:
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, ChannelSelectT]:
|
||||
...
|
||||
|
||||
|
||||
@ -967,7 +1039,8 @@ def select(
|
||||
disabled: bool = ...,
|
||||
default_values: Sequence[ValidDefaultValues] = ...,
|
||||
row: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[V, MentionableSelectT]:
|
||||
id: Optional[int] = ...,
|
||||
) -> SelectCallbackDecorator[S, MentionableSelectT]:
|
||||
...
|
||||
|
||||
|
||||
@ -983,7 +1056,8 @@ def select(
|
||||
disabled: bool = False,
|
||||
default_values: Sequence[ValidDefaultValues] = MISSING,
|
||||
row: Optional[int] = None,
|
||||
) -> SelectCallbackDecorator[V, BaseSelectT]:
|
||||
id: Optional[int] = None,
|
||||
) -> SelectCallbackDecorator[S, BaseSelectT]:
|
||||
"""A decorator that attaches a select menu to a component.
|
||||
|
||||
The function being decorated should have three parameters, ``self`` representing
|
||||
@ -1041,6 +1115,10 @@ def select(
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is ignored when used in a :class:`ActionRow` or v2 component.
|
||||
min_values: :class:`int`
|
||||
The minimum number of items that must be chosen for this select menu.
|
||||
Defaults to 1 and must be between 0 and 25.
|
||||
@ -1062,9 +1140,13 @@ def select(
|
||||
Number of items must be in range of ``min_values`` and ``max_values``.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]:
|
||||
def decorator(func: ItemCallbackType[S, BaseSelectT]) -> ItemCallbackType[S, BaseSelectT]:
|
||||
if not inspect.iscoroutinefunction(func):
|
||||
raise TypeError('select function must be a coroutine function')
|
||||
callback_cls = getattr(cls, '__origin__', cls)
|
||||
@ -1080,6 +1162,7 @@ def select(
|
||||
'min_values': min_values,
|
||||
'max_values': max_values,
|
||||
'disabled': disabled,
|
||||
'id': id,
|
||||
}
|
||||
if issubclass(callback_cls, Select):
|
||||
func.__discord_ui_model_kwargs__['options'] = options
|
||||
|
124
discord/ui/separator.py
Normal file
124
discord/ui/separator.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional, TypeVar
|
||||
|
||||
from .item import Item
|
||||
from ..components import SeparatorComponent
|
||||
from ..enums import SeparatorSpacing, ComponentType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .view import LayoutView
|
||||
|
||||
V = TypeVar('V', bound='LayoutView', covariant=True)
|
||||
|
||||
__all__ = ('Separator',)
|
||||
|
||||
|
||||
class Separator(Item[V]):
|
||||
"""Represents a UI separator.
|
||||
|
||||
This is a top-level layout component that can only be used on :class:`LayoutView`.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
visible: :class:`bool`
|
||||
Whether this separator is visible. On the client side this
|
||||
is whether a divider line should be shown or not.
|
||||
spacing: :class:`.SeparatorSpacing`
|
||||
The spacing of this separator.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
"""
|
||||
|
||||
__slots__ = ('_underlying',)
|
||||
__item_repr_attributes__ = (
|
||||
'visible',
|
||||
'spacing',
|
||||
'id',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
visible: bool = True,
|
||||
spacing: SeparatorSpacing = SeparatorSpacing.small,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._underlying = SeparatorComponent._raw_construct(
|
||||
spacing=spacing,
|
||||
visible=visible,
|
||||
id=id,
|
||||
)
|
||||
self.id = id
|
||||
|
||||
def _is_v2(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def visible(self) -> bool:
|
||||
""":class:`bool`: Whether this separator is visible.
|
||||
|
||||
On the client side this is whether a divider line should
|
||||
be shown or not.
|
||||
"""
|
||||
return self._underlying.visible
|
||||
|
||||
@visible.setter
|
||||
def visible(self, value: bool) -> None:
|
||||
self._underlying.visible = value
|
||||
|
||||
@property
|
||||
def spacing(self) -> SeparatorSpacing:
|
||||
""":class:`.SeparatorSpacing`: The spacing of this separator."""
|
||||
return self._underlying.spacing
|
||||
|
||||
@spacing.setter
|
||||
def spacing(self, value: SeparatorSpacing) -> None:
|
||||
self._underlying.spacing = value
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return 5
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.separator]:
|
||||
return self._underlying.type
|
||||
|
||||
def to_component_dict(self):
|
||||
return self._underlying.to_dict()
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: SeparatorComponent) -> Self:
|
||||
return cls(
|
||||
visible=component.visible,
|
||||
spacing=component.spacing,
|
||||
id=component.id,
|
||||
)
|
89
discord/ui/text_display.py
Normal file
89
discord/ui/text_display.py
Normal file
@ -0,0 +1,89 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional, TypeVar
|
||||
|
||||
from .item import Item
|
||||
from ..components import TextDisplay as TextDisplayComponent
|
||||
from ..enums import ComponentType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .view import LayoutView
|
||||
|
||||
V = TypeVar('V', bound='LayoutView', covariant=True)
|
||||
|
||||
__all__ = ('TextDisplay',)
|
||||
|
||||
|
||||
class TextDisplay(Item[V]):
|
||||
"""Represents a UI text display.
|
||||
|
||||
This is a top-level layout component that can only be used on :class:`LayoutView` or :class:`Section`.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
content: :class:`str`
|
||||
The content of this text display. Up to 4000 characters.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
"""
|
||||
|
||||
__slots__ = ('content',)
|
||||
|
||||
def __init__(self, content: str, *, row: Optional[int] = None, id: Optional[int] = None) -> None:
|
||||
super().__init__()
|
||||
self.content: str = content
|
||||
self.id = id
|
||||
|
||||
def to_component_dict(self):
|
||||
base = {
|
||||
'type': self.type.value,
|
||||
'content': self.content,
|
||||
}
|
||||
if self.id is not None:
|
||||
base['id'] = self.id
|
||||
return base
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return 5
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.text_display]:
|
||||
return ComponentType.text_display
|
||||
|
||||
def _is_v2(self) -> bool:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: TextDisplayComponent) -> Self:
|
||||
return cls(
|
||||
content=component.content,
|
||||
id=component.id,
|
||||
)
|
@ -92,12 +92,17 @@ class TextInput(Item[V]):
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: Tuple[str, ...] = (
|
||||
'label',
|
||||
'placeholder',
|
||||
'required',
|
||||
'id',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@ -112,6 +117,7 @@ class TextInput(Item[V]):
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
row: Optional[int] = None,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._value: Optional[str] = default
|
||||
@ -129,8 +135,10 @@ class TextInput(Item[V]):
|
||||
required=required,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
id=id,
|
||||
)
|
||||
self.row = row
|
||||
self.id = id
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@ -241,6 +249,7 @@ class TextInput(Item[V]):
|
||||
min_length=component.min_length,
|
||||
max_length=component.max_length,
|
||||
row=None,
|
||||
id=component.id,
|
||||
)
|
||||
|
||||
@property
|
||||
|
132
discord/ui/thumbnail.py
Normal file
132
discord/ui/thumbnail.py
Normal file
@ -0,0 +1,132 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar, Union
|
||||
|
||||
from .item import Item
|
||||
from ..enums import ComponentType
|
||||
from ..components import UnfurledMediaItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from .view import LayoutView
|
||||
from ..components import ThumbnailComponent
|
||||
|
||||
V = TypeVar('V', bound='LayoutView', covariant=True)
|
||||
|
||||
__all__ = ('Thumbnail',)
|
||||
|
||||
|
||||
class Thumbnail(Item[V]):
|
||||
"""Represents a UI Thumbnail. This currently can only be used as a :class:`Section`\'s accessory.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
media: Union[:class:`str`, :class:`discord.UnfurledMediaItem`]
|
||||
The media of the thumbnail. This can be a URL or a reference
|
||||
to an attachment that matches the ``attachment://filename.extension``
|
||||
structure.
|
||||
description: Optional[:class:`str`]
|
||||
The description of this thumbnail. Up to 256 characters. Defaults to ``None``.
|
||||
spoiler: :class:`bool`
|
||||
Whether to flag this thumbnail as a spoiler. Defaults to ``False``.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component. This must be unique across the view.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_media',
|
||||
'description',
|
||||
'spoiler',
|
||||
)
|
||||
__item_repr_attributes__ = (
|
||||
'media',
|
||||
'description',
|
||||
'spoiler',
|
||||
'row',
|
||||
'id',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
media: Union[str, UnfurledMediaItem],
|
||||
*,
|
||||
description: Optional[str] = None,
|
||||
spoiler: bool = False,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media
|
||||
self.description: Optional[str] = description
|
||||
self.spoiler: bool = spoiler
|
||||
self.id = id
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return 5
|
||||
|
||||
@property
|
||||
def media(self) -> UnfurledMediaItem:
|
||||
""":class:`discord.UnfurledMediaItem`: This thumbnail unfurled media data."""
|
||||
return self._media
|
||||
|
||||
@media.setter
|
||||
def media(self, value: Union[str, UnfurledMediaItem]) -> None:
|
||||
if isinstance(value, str):
|
||||
self._media = UnfurledMediaItem(value)
|
||||
elif isinstance(value, UnfurledMediaItem):
|
||||
self._media = value
|
||||
else:
|
||||
raise TypeError(f'expected a str or UnfurledMediaItem, not {value.__class__.__name__!r}')
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.thumbnail]:
|
||||
return ComponentType.thumbnail
|
||||
|
||||
def _is_v2(self) -> bool:
|
||||
return True
|
||||
|
||||
def to_component_dict(self) -> Dict[str, Any]:
|
||||
base = {
|
||||
'type': self.type.value,
|
||||
'spoiler': self.spoiler,
|
||||
'media': self.media.to_dict(),
|
||||
'description': self.description,
|
||||
}
|
||||
if self.id is not None:
|
||||
base['id'] = self.id
|
||||
return base
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: ThumbnailComponent) -> Self:
|
||||
return cls(
|
||||
media=component.media.url,
|
||||
description=component.description,
|
||||
spoiler=component.spoiler,
|
||||
id=component.id,
|
||||
)
|
@ -23,7 +23,23 @@ DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Generator,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
|
||||
@ -32,6 +48,7 @@ import logging
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
|
||||
from .item import Item, ItemCallbackType
|
||||
from .select import Select
|
||||
from .dynamic import DynamicItem
|
||||
@ -41,26 +58,37 @@ from ..components import (
|
||||
_component_factory,
|
||||
Button as ButtonComponent,
|
||||
SelectMenu as SelectComponent,
|
||||
SectionComponent,
|
||||
TextDisplay as TextDisplayComponent,
|
||||
MediaGalleryComponent,
|
||||
FileComponent,
|
||||
SeparatorComponent,
|
||||
ThumbnailComponent,
|
||||
Container as ContainerComponent,
|
||||
)
|
||||
from ..utils import get as _utils_get, find as _utils_find
|
||||
|
||||
# fmt: off
|
||||
__all__ = (
|
||||
'View',
|
||||
'LayoutView',
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
from typing_extensions import Self, TypeGuard
|
||||
import re
|
||||
|
||||
from ..interactions import Interaction
|
||||
from ..message import Message
|
||||
from ..types.components import Component as ComponentPayload
|
||||
from ..types.components import ComponentBase as ComponentBasePayload, Component as ComponentPayload
|
||||
from ..types.interactions import ModalSubmitComponentInteractionData as ModalSubmitComponentInteractionDataPayload
|
||||
from ..state import ConnectionState
|
||||
from .modal import Modal
|
||||
|
||||
ItemLike = Union[ItemCallbackType[Any, Any], Item[Any]]
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
@ -69,21 +97,61 @@ def _walk_all_components(components: List[Component]) -> Iterator[Component]:
|
||||
for item in components:
|
||||
if isinstance(item, ActionRowComponent):
|
||||
yield from item.children
|
||||
elif isinstance(item, ContainerComponent):
|
||||
yield from _walk_all_components(item.children)
|
||||
elif isinstance(item, SectionComponent):
|
||||
yield from item.children
|
||||
yield item.accessory
|
||||
else:
|
||||
yield item
|
||||
|
||||
|
||||
def _component_to_item(component: Component) -> Item:
|
||||
if isinstance(component, ButtonComponent):
|
||||
def _component_to_item(component: Component, parent: Optional[Item] = None) -> Item:
|
||||
if isinstance(component, ActionRowComponent):
|
||||
from .action_row import ActionRow
|
||||
|
||||
item = ActionRow.from_component(component)
|
||||
elif isinstance(component, ButtonComponent):
|
||||
from .button import Button
|
||||
|
||||
return Button.from_component(component)
|
||||
if isinstance(component, SelectComponent):
|
||||
item = Button.from_component(component)
|
||||
elif isinstance(component, SelectComponent):
|
||||
from .select import BaseSelect
|
||||
|
||||
return BaseSelect.from_component(component)
|
||||
item = BaseSelect.from_component(component)
|
||||
elif isinstance(component, SectionComponent):
|
||||
from .section import Section
|
||||
|
||||
return Item.from_component(component)
|
||||
item = Section.from_component(component)
|
||||
elif isinstance(component, TextDisplayComponent):
|
||||
from .text_display import TextDisplay
|
||||
|
||||
item = TextDisplay.from_component(component)
|
||||
elif isinstance(component, MediaGalleryComponent):
|
||||
from .media_gallery import MediaGallery
|
||||
|
||||
item = MediaGallery.from_component(component)
|
||||
elif isinstance(component, FileComponent):
|
||||
from .file import File
|
||||
|
||||
item = File.from_component(component)
|
||||
elif isinstance(component, SeparatorComponent):
|
||||
from .separator import Separator
|
||||
|
||||
item = Separator.from_component(component)
|
||||
elif isinstance(component, ThumbnailComponent):
|
||||
from .thumbnail import Thumbnail
|
||||
|
||||
item = Thumbnail.from_component(component)
|
||||
elif isinstance(component, ContainerComponent):
|
||||
from .container import Container
|
||||
|
||||
item = Container.from_component(component)
|
||||
else:
|
||||
item = Item.from_component(component)
|
||||
|
||||
item._parent = parent
|
||||
return item
|
||||
|
||||
|
||||
class _ViewWeights:
|
||||
@ -133,73 +201,66 @@ class _ViewWeights:
|
||||
class _ViewCallback:
|
||||
__slots__ = ('view', 'callback', 'item')
|
||||
|
||||
def __init__(self, callback: ItemCallbackType[Any, Any], view: View, item: Item[View]) -> None:
|
||||
def __init__(self, callback: ItemCallbackType[Any, Any], view: BaseView, item: Item[BaseView]) -> None:
|
||||
self.callback: ItemCallbackType[Any, Any] = callback
|
||||
self.view: View = view
|
||||
self.item: Item[View] = item
|
||||
self.view: BaseView = view
|
||||
self.item: Item[BaseView] = item
|
||||
|
||||
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
|
||||
return self.callback(self.view, interaction, self.item)
|
||||
|
||||
|
||||
class View:
|
||||
"""Represents a UI view.
|
||||
|
||||
This object must be inherited to create a UI within Discord.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
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.
|
||||
"""
|
||||
|
||||
__discord_ui_view__: ClassVar[bool] = True
|
||||
class BaseView:
|
||||
__discord_ui_view__: ClassVar[bool] = False
|
||||
__discord_ui_modal__: ClassVar[bool] = False
|
||||
__view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = []
|
||||
__view_children_items__: ClassVar[Dict[str, ItemLike]] = {}
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
|
||||
children: Dict[str, ItemCallbackType[Any, Any]] = {}
|
||||
for base in reversed(cls.__mro__):
|
||||
for name, member in base.__dict__.items():
|
||||
if hasattr(member, '__discord_ui_model_type__'):
|
||||
children[name] = member
|
||||
|
||||
if len(children) > 25:
|
||||
raise TypeError('View cannot have more than 25 children')
|
||||
|
||||
cls.__view_children_items__ = list(children.values())
|
||||
|
||||
def _init_children(self) -> List[Item[Self]]:
|
||||
children = []
|
||||
for func in self.__view_children_items__:
|
||||
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
|
||||
item.callback = _ViewCallback(func, self, item) # type: ignore
|
||||
item._view = self
|
||||
if isinstance(item, Select):
|
||||
item.options = [option.copy() for option in item.options]
|
||||
setattr(self, func.__name__, item)
|
||||
children.append(item)
|
||||
return children
|
||||
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0):
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
|
||||
self.__timeout = timeout
|
||||
self._children: List[Item[Self]] = self._init_children()
|
||||
self.__weights = _ViewWeights(self._children)
|
||||
self.id: str = os.urandom(16).hex()
|
||||
self._cache_key: Optional[int] = None
|
||||
self.__cancel_callback: Optional[Callable[[View], None]] = None
|
||||
self.__cancel_callback: Optional[Callable[[BaseView], None]] = None
|
||||
self.__timeout_expiry: Optional[float] = None
|
||||
self.__timeout_task: Optional[asyncio.Task[None]] = None
|
||||
self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
|
||||
self._total_children: int = len(tuple(self.walk_children()))
|
||||
|
||||
def _is_layout(self) -> TypeGuard[LayoutView]: # type: ignore
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>'
|
||||
|
||||
def _init_children(self) -> List[Item[Self]]:
|
||||
children = []
|
||||
parents = {}
|
||||
|
||||
for name, raw in self.__view_children_items__.items():
|
||||
if isinstance(raw, Item):
|
||||
item = raw.copy()
|
||||
setattr(self, name, item)
|
||||
item._update_view(self)
|
||||
parent = getattr(item, '__discord_ui_parent__', None)
|
||||
if parent and parent._view is None:
|
||||
parent._view = self
|
||||
children.append(item)
|
||||
parents[raw] = item
|
||||
else:
|
||||
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
|
||||
item.callback = _ViewCallback(raw, self, item) # type: ignore
|
||||
item._view = self
|
||||
if isinstance(item, Select):
|
||||
item.options = [option.copy() for option in item.options]
|
||||
setattr(self, raw.__name__, item)
|
||||
parent = getattr(raw, '__discord_ui_parent__', None)
|
||||
if parent:
|
||||
parents.get(parent, parent)._children.append(item)
|
||||
continue
|
||||
children.append(item)
|
||||
|
||||
return children
|
||||
|
||||
async def __timeout_task_impl(self) -> None:
|
||||
while True:
|
||||
# Guard just in case someone changes the value of the timeout at runtime
|
||||
@ -218,29 +279,16 @@ class View:
|
||||
await asyncio.sleep(self.__timeout_expiry - now)
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
# this is used by webhooks to check whether a view requires a state attached
|
||||
# or not, this simply is, whether a view has a component other than a url button
|
||||
return any(item.is_dispatchable() for item in self.children)
|
||||
# checks whether any interactable items (buttons or selects) are present
|
||||
# in this view, and check whether this requires a state attached in case
|
||||
# of webhooks and if the view should be stored in the view store
|
||||
return any(item.is_dispatchable() for item in self.walk_children())
|
||||
|
||||
def has_components_v2(self) -> bool:
|
||||
return any(c._is_v2() for c in self.children)
|
||||
|
||||
def to_components(self) -> List[Dict[str, Any]]:
|
||||
def key(item: Item) -> int:
|
||||
return item._rendered_row or 0
|
||||
|
||||
children = sorted(self._children, key=key)
|
||||
components: List[Dict[str, Any]] = []
|
||||
for _, 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
|
||||
return NotImplemented
|
||||
|
||||
def _refresh_timeout(self) -> None:
|
||||
if self.__timeout:
|
||||
@ -271,13 +319,17 @@ class View:
|
||||
return self._children.copy()
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View:
|
||||
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> Union[View, LayoutView]:
|
||||
"""Converts a message's components into a :class:`View`.
|
||||
|
||||
The :attr:`.Message.components` of a message are read-only
|
||||
and separate types from those in the ``discord.ui`` namespace.
|
||||
In order to modify and edit message components they must be
|
||||
converted into a :class:`View` first.
|
||||
converted into a :class:`View` or :class:`LayoutView` first.
|
||||
|
||||
If the message has any v2 components, then you must use
|
||||
:class:`LayoutView` in order for them to be converted into
|
||||
their respective items. :class:`View` does not support v2 components.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
@ -287,24 +339,43 @@ class View:
|
||||
The timeout of the converted view.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`View`
|
||||
The converted view. This always returns a :class:`View` and not
|
||||
one of its subclasses.
|
||||
-------
|
||||
Union[:class:`View`, :class:`LayoutView`]
|
||||
The converted view. This will always return one of :class:`View` or
|
||||
:class:`LayoutView`, and not one of its subclasses.
|
||||
"""
|
||||
view = View(timeout=timeout)
|
||||
|
||||
if issubclass(cls, View):
|
||||
view_cls = View
|
||||
elif issubclass(cls, LayoutView):
|
||||
view_cls = LayoutView
|
||||
else:
|
||||
raise TypeError('unreachable exception')
|
||||
|
||||
view = view_cls(timeout=timeout)
|
||||
row = 0
|
||||
|
||||
for component in message.components:
|
||||
if isinstance(component, ActionRowComponent):
|
||||
if not view._is_layout() and isinstance(component, ActionRowComponent):
|
||||
for child in component.children:
|
||||
item = _component_to_item(child)
|
||||
item.row = row
|
||||
# this error should never be raised, because ActionRows can only
|
||||
# contain items that View accepts, but check anyways
|
||||
if item._is_v2():
|
||||
raise ValueError(f'{item.__class__.__name__} cannot be added to {view.__class__.__name__}')
|
||||
view.add_item(item)
|
||||
row += 1
|
||||
else:
|
||||
item = _component_to_item(component)
|
||||
item.row = row
|
||||
view.add_item(item)
|
||||
row += 1
|
||||
continue
|
||||
|
||||
item = _component_to_item(component)
|
||||
item.row = row
|
||||
|
||||
if item._is_v2() and not view._is_layout():
|
||||
raise ValueError(f'{item.__class__.__name__} cannot be added to {view.__class__.__name__}')
|
||||
|
||||
view.add_item(item)
|
||||
row += 1
|
||||
|
||||
return view
|
||||
|
||||
@ -324,19 +395,25 @@ class View:
|
||||
TypeError
|
||||
An :class:`Item` was not passed.
|
||||
ValueError
|
||||
Maximum number of children has been exceeded (25)
|
||||
or the row the item is trying to be added to is full.
|
||||
Maximum number of children has been exceeded, the
|
||||
row the item is trying to be added to is full or the item
|
||||
you tried to add is not allowed in this View.
|
||||
"""
|
||||
|
||||
if len(self._children) >= 25:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
|
||||
if not isinstance(item, Item):
|
||||
raise TypeError(f'expected Item not {item.__class__.__name__}')
|
||||
if item._is_v2() and not self._is_layout():
|
||||
raise ValueError('v2 items cannot be added to this view')
|
||||
|
||||
self.__weights.add_item(item)
|
||||
item._update_view(self)
|
||||
added = 1
|
||||
|
||||
item._view = self
|
||||
if item._has_children():
|
||||
added += len(tuple(item.walk_children())) # type: ignore
|
||||
|
||||
if self._is_layout() and self._total_children + added > 40:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
self._total_children += added
|
||||
self._children.append(item)
|
||||
return self
|
||||
|
||||
@ -357,7 +434,15 @@ class View:
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self.__weights.remove_item(item)
|
||||
removed = 1
|
||||
if item._has_children():
|
||||
removed += len(tuple(item.walk_children())) # type: ignore
|
||||
|
||||
if self._total_children - removed < 0:
|
||||
self._total_children = 0
|
||||
else:
|
||||
self._total_children -= removed
|
||||
|
||||
return self
|
||||
|
||||
def clear_items(self) -> Self:
|
||||
@ -367,9 +452,31 @@ class View:
|
||||
chaining.
|
||||
"""
|
||||
self._children.clear()
|
||||
self.__weights.clear()
|
||||
self._total_children = 0
|
||||
return self
|
||||
|
||||
def find_item(self, id: int, /) -> Optional[Item[Self]]:
|
||||
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
|
||||
not found.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is **not the same** as ``custom_id``.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id: :class:`int`
|
||||
The ID of the component.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`Item`]
|
||||
The item found, or ``None``.
|
||||
"""
|
||||
return _utils_get(self.walk_children(), id=id)
|
||||
|
||||
async def interaction_check(self, interaction: Interaction, /) -> bool:
|
||||
"""|coro|
|
||||
|
||||
@ -428,7 +535,7 @@ class View:
|
||||
try:
|
||||
item._refresh_state(interaction, interaction.data) # type: ignore
|
||||
|
||||
allow = await item.interaction_check(interaction) and await self.interaction_check(interaction)
|
||||
allow = await item._run_checks(interaction) and await self.interaction_check(interaction)
|
||||
if not allow:
|
||||
return
|
||||
|
||||
@ -440,7 +547,7 @@ class View:
|
||||
return await self.on_error(interaction, e, item)
|
||||
|
||||
def _start_listening_from_store(self, store: ViewStore) -> None:
|
||||
self.__cancel_callback = partial(store.remove_view)
|
||||
self.__cancel_callback = partial(store.remove_view) # type: ignore
|
||||
if self.timeout:
|
||||
if self.__timeout_task is not None:
|
||||
self.__timeout_task.cancel()
|
||||
@ -469,7 +576,7 @@ class View:
|
||||
# fmt: off
|
||||
old_state: Dict[str, Item[Any]] = {
|
||||
item.custom_id: item # type: ignore
|
||||
for item in self._children
|
||||
for item in self.walk_children()
|
||||
if item.is_dispatchable()
|
||||
}
|
||||
# fmt: on
|
||||
@ -536,13 +643,193 @@ class View:
|
||||
"""
|
||||
return await self.__stopped
|
||||
|
||||
def walk_children(self) -> Generator[Item[Any], None, None]:
|
||||
"""An iterator that recursively walks through all the children of this view
|
||||
and its children, if applicable.
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`Item`
|
||||
An item in the view.
|
||||
"""
|
||||
|
||||
for child in self.children:
|
||||
yield child
|
||||
|
||||
if child._has_children():
|
||||
yield from child.walk_children() # type: ignore
|
||||
|
||||
|
||||
class View(BaseView):
|
||||
"""Represents a UI view.
|
||||
|
||||
This object must be inherited to create a UI within Discord.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
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.
|
||||
"""
|
||||
|
||||
__discord_ui_view__: ClassVar[bool] = True
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: List[ComponentPayload], *, timeout: Optional[float] = 180.0) -> View:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View:
|
||||
...
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
|
||||
children: Dict[str, ItemLike] = {}
|
||||
for base in reversed(cls.__mro__):
|
||||
for name, member in base.__dict__.items():
|
||||
if hasattr(member, '__discord_ui_model_type__'):
|
||||
children[name] = member
|
||||
elif isinstance(member, Item) and member._is_v2():
|
||||
raise ValueError(f'{name} cannot be added to this View')
|
||||
|
||||
if len(children) > 25:
|
||||
raise TypeError('View cannot have more than 25 children')
|
||||
|
||||
cls.__view_children_items__ = children
|
||||
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0):
|
||||
super().__init__(timeout=timeout)
|
||||
self.__weights = _ViewWeights(self._children)
|
||||
|
||||
def to_components(self) -> List[Dict[str, Any]]:
|
||||
def key(item: Item) -> int:
|
||||
return item._rendered_row or 0
|
||||
|
||||
children = sorted(self._children, key=key)
|
||||
components: List[Dict[str, Any]] = []
|
||||
for _, 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: Item[Any]) -> Self:
|
||||
if len(self._children) >= 25:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
|
||||
super().add_item(item)
|
||||
try:
|
||||
self.__weights.add_item(item)
|
||||
except ValueError as e:
|
||||
# if the item has no space left then remove it from _children
|
||||
self._children.remove(item)
|
||||
raise e
|
||||
|
||||
return self
|
||||
|
||||
def remove_item(self, item: Item[Any]) -> Self:
|
||||
try:
|
||||
self._children.remove(item)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self.__weights.remove_item(item)
|
||||
return self
|
||||
|
||||
def clear_items(self) -> Self:
|
||||
super().clear_items()
|
||||
self.__weights.clear()
|
||||
return self
|
||||
|
||||
|
||||
class LayoutView(BaseView):
|
||||
"""Represents a layout view for components.
|
||||
|
||||
This object must be inherited to create a UI within Discord.
|
||||
|
||||
You can find usage examples in the :resource:`repository <examples>`
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
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.
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: List[ComponentPayload], *, timeout: Optional[float] = 180.0) -> LayoutView:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> LayoutView:
|
||||
...
|
||||
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
if self._total_children > 40:
|
||||
raise ValueError('maximum number of children exceeded (40)')
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
|
||||
children: Dict[str, ItemLike] = {}
|
||||
callback_children: Dict[str, ItemCallbackType[Any, Any]] = {}
|
||||
|
||||
for base in reversed(cls.__mro__):
|
||||
for name, member in base.__dict__.items():
|
||||
if isinstance(member, Item):
|
||||
if member._parent is not None:
|
||||
continue
|
||||
|
||||
member._rendered_row = member._row
|
||||
children[name] = member
|
||||
elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None):
|
||||
callback_children[name] = member
|
||||
|
||||
children.update(callback_children)
|
||||
cls.__view_children_items__ = children
|
||||
|
||||
def _is_layout(self) -> TypeGuard[LayoutView]: # type: ignore
|
||||
return True
|
||||
|
||||
def to_components(self):
|
||||
components: List[Dict[str, Any]] = []
|
||||
for i in self._children:
|
||||
components.append(i.to_component_dict())
|
||||
|
||||
return components
|
||||
|
||||
def add_item(self, item: Item[Any]) -> Self:
|
||||
if self._total_children >= 40:
|
||||
raise ValueError('maximum number of children exceeded (40)')
|
||||
super().add_item(item)
|
||||
return self
|
||||
|
||||
|
||||
class ViewStore:
|
||||
def __init__(self, state: ConnectionState):
|
||||
# entity_id: {(component_type, custom_id): Item}
|
||||
self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[View]]] = {}
|
||||
self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[BaseView]]] = {}
|
||||
# message_id: View
|
||||
self._synced_message_views: Dict[int, View] = {}
|
||||
self._synced_message_views: Dict[int, BaseView] = {}
|
||||
# custom_id: Modal
|
||||
self._modals: Dict[str, Modal] = {}
|
||||
# component_type is the key
|
||||
@ -550,7 +837,7 @@ class ViewStore:
|
||||
self._state: ConnectionState = state
|
||||
|
||||
@property
|
||||
def persistent_views(self) -> Sequence[View]:
|
||||
def persistent_views(self) -> Sequence[BaseView]:
|
||||
# fmt: off
|
||||
views = {
|
||||
item.view.id: item.view
|
||||
@ -571,7 +858,7 @@ class ViewStore:
|
||||
pattern = item.__discord_ui_compiled_template__
|
||||
self._dynamic_items.pop(pattern, None)
|
||||
|
||||
def add_view(self, view: View, message_id: Optional[int] = None) -> None:
|
||||
def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None:
|
||||
view._start_listening_from_store(self)
|
||||
if view.__discord_ui_modal__:
|
||||
self._modals[view.custom_id] = view # type: ignore
|
||||
@ -579,7 +866,7 @@ class ViewStore:
|
||||
|
||||
dispatch_info = self._views.setdefault(message_id, {})
|
||||
is_fully_dynamic = True
|
||||
for item in view._children:
|
||||
for item in view.walk_children():
|
||||
if isinstance(item, DynamicItem):
|
||||
pattern = item.__discord_ui_compiled_template__
|
||||
self._dynamic_items[pattern] = item.__class__
|
||||
@ -621,15 +908,16 @@ class ViewStore:
|
||||
if interaction.message is None:
|
||||
return
|
||||
|
||||
view = View.from_message(interaction.message, timeout=None)
|
||||
view_cls = View if not interaction.message.flags.components_v2 else LayoutView
|
||||
view = view_cls.from_message(interaction.message, timeout=None)
|
||||
|
||||
try:
|
||||
base_item_index, base_item = next(
|
||||
(index, child)
|
||||
for index, child in enumerate(view._children)
|
||||
if child.type.value == component_type and getattr(child, 'custom_id', None) == custom_id
|
||||
)
|
||||
except StopIteration:
|
||||
base_item = _utils_find(
|
||||
lambda i: i.type.value == component_type and getattr(i, 'custom_id', None) == custom_id,
|
||||
view.walk_children(),
|
||||
)
|
||||
|
||||
# if the item is not found then return
|
||||
if not base_item:
|
||||
return
|
||||
|
||||
try:
|
||||
@ -638,8 +926,17 @@ class ViewStore:
|
||||
_log.exception('Ignoring exception in dynamic item creation for %r', factory)
|
||||
return
|
||||
|
||||
# Swap the item in the view with our new dynamic item
|
||||
view._children[base_item_index] = item
|
||||
# Swap the item in the view or parent with our new dynamic item
|
||||
# Prioritize the item parent:
|
||||
parent = base_item._parent or view
|
||||
|
||||
try:
|
||||
child_index = parent._children.index(base_item) # type: ignore
|
||||
except ValueError:
|
||||
return
|
||||
else:
|
||||
parent._children[child_index] = item # type: ignore
|
||||
|
||||
item._view = view
|
||||
item._rendered_row = base_item._rendered_row
|
||||
item._refresh_state(interaction, interaction.data) # type: ignore
|
||||
@ -681,7 +978,7 @@ class ViewStore:
|
||||
key = (component_type, custom_id)
|
||||
|
||||
# The entity_id can either be message_id, interaction_id, or None in that priority order.
|
||||
item: Optional[Item[View]] = None
|
||||
item: Optional[Item[BaseView]] = None
|
||||
if message_id is not None:
|
||||
item = self._views.get(message_id, {}).get(key)
|
||||
|
||||
@ -733,14 +1030,14 @@ class ViewStore:
|
||||
def is_message_tracked(self, message_id: int) -> bool:
|
||||
return message_id in self._synced_message_views
|
||||
|
||||
def remove_message_tracking(self, message_id: int) -> Optional[View]:
|
||||
def remove_message_tracking(self, message_id: int) -> Optional[BaseView]:
|
||||
return self._synced_message_views.pop(message_id, None)
|
||||
|
||||
def update_from_message(self, message_id: int, data: List[ComponentPayload]) -> None:
|
||||
def update_from_message(self, message_id: int, data: List[ComponentBasePayload]) -> None:
|
||||
components: List[Component] = []
|
||||
|
||||
for component_data in data:
|
||||
component = _component_factory(component_data)
|
||||
component = _component_factory(component_data, self._state) # type: ignore
|
||||
|
||||
if component is not None:
|
||||
components.append(component)
|
||||
|
@ -71,7 +71,7 @@ if TYPE_CHECKING:
|
||||
from ..emoji import Emoji
|
||||
from ..channel import VoiceChannel
|
||||
from ..abc import Snowflake
|
||||
from ..ui.view import View
|
||||
from ..ui.view import BaseView, View, LayoutView
|
||||
from ..poll import Poll
|
||||
import datetime
|
||||
from ..types.webhook import (
|
||||
@ -552,7 +552,7 @@ def interaction_message_response_params(
|
||||
embed: Optional[Embed] = MISSING,
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
view: Optional[BaseView] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = MISSING,
|
||||
previous_allowed_mentions: Optional[AllowedMentions] = None,
|
||||
poll: Poll = MISSING,
|
||||
@ -592,6 +592,13 @@ def interaction_message_response_params(
|
||||
if view is not MISSING:
|
||||
if view is not None:
|
||||
data['components'] = view.to_components()
|
||||
|
||||
if view.has_components_v2():
|
||||
if flags is not MISSING:
|
||||
flags.components_v2 = True
|
||||
else:
|
||||
flags = MessageFlags(components_v2=True)
|
||||
|
||||
else:
|
||||
data['components'] = []
|
||||
|
||||
@ -802,7 +809,7 @@ class WebhookMessage(Message):
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
embed: Optional[Embed] = MISSING,
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
view: Optional[BaseView] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
) -> WebhookMessage:
|
||||
"""|coro|
|
||||
@ -1598,6 +1605,46 @@ class Webhook(BaseWebhook):
|
||||
# state is artificial
|
||||
return WebhookMessage(data=data, state=state, channel=channel) # type: ignore
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
*,
|
||||
username: str = MISSING,
|
||||
avatar_url: Any = MISSING,
|
||||
ephemeral: bool = MISSING,
|
||||
file: File = MISSING,
|
||||
files: Sequence[File] = MISSING,
|
||||
allowed_mentions: AllowedMentions = MISSING,
|
||||
view: LayoutView,
|
||||
wait: Literal[True],
|
||||
thread: Snowflake = MISSING,
|
||||
thread_name: str = MISSING,
|
||||
suppress_embeds: bool = MISSING,
|
||||
silent: bool = MISSING,
|
||||
applied_tags: List[ForumTag] = MISSING,
|
||||
) -> WebhookMessage:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
*,
|
||||
username: str = MISSING,
|
||||
avatar_url: Any = MISSING,
|
||||
ephemeral: bool = MISSING,
|
||||
file: File = MISSING,
|
||||
files: Sequence[File] = MISSING,
|
||||
allowed_mentions: AllowedMentions = MISSING,
|
||||
view: LayoutView,
|
||||
wait: Literal[False] = ...,
|
||||
thread: Snowflake = MISSING,
|
||||
thread_name: str = MISSING,
|
||||
suppress_embeds: bool = MISSING,
|
||||
silent: bool = MISSING,
|
||||
applied_tags: List[ForumTag] = MISSING,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def send(
|
||||
self,
|
||||
@ -1661,7 +1708,7 @@ class Webhook(BaseWebhook):
|
||||
embed: Embed = MISSING,
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
allowed_mentions: AllowedMentions = MISSING,
|
||||
view: View = MISSING,
|
||||
view: BaseView = MISSING,
|
||||
thread: Snowflake = MISSING,
|
||||
thread_name: str = MISSING,
|
||||
wait: bool = False,
|
||||
@ -1727,7 +1774,7 @@ class Webhook(BaseWebhook):
|
||||
Controls the mentions being processed in this message.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
view: :class:`discord.ui.View`
|
||||
view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`]
|
||||
The view to send with the message. If the webhook is partial or
|
||||
is not managed by the library, then you can only send URL buttons.
|
||||
Otherwise, you can send views with any type of components.
|
||||
@ -1931,6 +1978,33 @@ class Webhook(BaseWebhook):
|
||||
)
|
||||
return self._create_message(data, thread=thread)
|
||||
|
||||
@overload
|
||||
async def edit_message(
|
||||
self,
|
||||
message_id: int,
|
||||
*,
|
||||
attachments: Sequence[Union[Attachment, File]] = ...,
|
||||
view: LayoutView,
|
||||
allowed_mentions: Optional[AllowedMentions] = ...,
|
||||
thread: Snowflake = ...,
|
||||
) -> WebhookMessage:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def edit_message(
|
||||
self,
|
||||
message_id: int,
|
||||
*,
|
||||
content: Optional[str] = ...,
|
||||
embeds: Sequence[Embed] = ...,
|
||||
embed: Optional[Embed] = ...,
|
||||
attachments: Sequence[Union[Attachment, File]] = ...,
|
||||
view: Optional[View] = ...,
|
||||
allowed_mentions: Optional[AllowedMentions] = ...,
|
||||
thread: Snowflake = ...,
|
||||
) -> WebhookMessage:
|
||||
...
|
||||
|
||||
async def edit_message(
|
||||
self,
|
||||
message_id: int,
|
||||
@ -1939,7 +2013,7 @@ class Webhook(BaseWebhook):
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
embed: Optional[Embed] = MISSING,
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
view: Optional[View] = MISSING,
|
||||
view: Optional[BaseView] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
thread: Snowflake = MISSING,
|
||||
) -> WebhookMessage:
|
||||
@ -1978,11 +2052,17 @@ class Webhook(BaseWebhook):
|
||||
allowed_mentions: :class:`AllowedMentions`
|
||||
Controls the mentions being processed in this message.
|
||||
See :meth:`.abc.Messageable.send` for more information.
|
||||
view: Optional[:class:`~discord.ui.View`]
|
||||
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
|
||||
The updated view to update this message with. If ``None`` is passed then
|
||||
the view is removed. The webhook must have state attached, similar to
|
||||
:meth:`send`.
|
||||
|
||||
.. note::
|
||||
|
||||
To update the message to add a :class:`~discord.ui.LayoutView`, you
|
||||
must explicitly set the ``content``, ``embed``, ``embeds``, and
|
||||
``attachments`` parameters to either ``None`` or an empty array, as appropriate.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
thread: :class:`~discord.abc.Snowflake`
|
||||
The thread the webhook message belongs to.
|
||||
@ -2046,7 +2126,7 @@ class Webhook(BaseWebhook):
|
||||
)
|
||||
|
||||
message = self._create_message(data, thread=thread)
|
||||
if view and not view.is_finished():
|
||||
if view and not view.is_finished() and view.is_dispatchable():
|
||||
self._state.store_view(view, message_id)
|
||||
return message
|
||||
|
||||
|
@ -66,7 +66,7 @@ if TYPE_CHECKING:
|
||||
from ..message import Attachment
|
||||
from ..abc import Snowflake
|
||||
from ..state import ConnectionState
|
||||
from ..ui import View
|
||||
from ..ui.view import BaseView, View, LayoutView
|
||||
from ..types.webhook import (
|
||||
Webhook as WebhookPayload,
|
||||
)
|
||||
@ -856,6 +856,44 @@ class SyncWebhook(BaseWebhook):
|
||||
# state is artificial
|
||||
return SyncWebhookMessage(data=data, state=state, channel=channel) # type: ignore
|
||||
|
||||
@overload
|
||||
def send(
|
||||
self,
|
||||
*,
|
||||
username: str = MISSING,
|
||||
avatar_url: Any = MISSING,
|
||||
file: File = MISSING,
|
||||
files: Sequence[File] = MISSING,
|
||||
allowed_mentions: AllowedMentions = MISSING,
|
||||
view: LayoutView,
|
||||
wait: Literal[True],
|
||||
thread: Snowflake = MISSING,
|
||||
thread_name: str = MISSING,
|
||||
suppress_embeds: bool = MISSING,
|
||||
silent: bool = MISSING,
|
||||
applied_tags: List[ForumTag] = MISSING,
|
||||
) -> SyncWebhookMessage:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send(
|
||||
self,
|
||||
*,
|
||||
username: str = MISSING,
|
||||
avatar_url: Any = MISSING,
|
||||
file: File = MISSING,
|
||||
files: Sequence[File] = MISSING,
|
||||
allowed_mentions: AllowedMentions = MISSING,
|
||||
view: LayoutView,
|
||||
wait: Literal[False] = ...,
|
||||
thread: Snowflake = MISSING,
|
||||
thread_name: str = MISSING,
|
||||
suppress_embeds: bool = MISSING,
|
||||
silent: bool = MISSING,
|
||||
applied_tags: List[ForumTag] = MISSING,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send(
|
||||
self,
|
||||
@ -876,6 +914,7 @@ class SyncWebhook(BaseWebhook):
|
||||
silent: bool = MISSING,
|
||||
applied_tags: List[ForumTag] = MISSING,
|
||||
poll: Poll = MISSING,
|
||||
view: View = MISSING,
|
||||
) -> SyncWebhookMessage:
|
||||
...
|
||||
|
||||
@ -899,6 +938,7 @@ class SyncWebhook(BaseWebhook):
|
||||
silent: bool = MISSING,
|
||||
applied_tags: List[ForumTag] = MISSING,
|
||||
poll: Poll = MISSING,
|
||||
view: View = MISSING,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@ -921,7 +961,7 @@ class SyncWebhook(BaseWebhook):
|
||||
silent: bool = False,
|
||||
applied_tags: List[ForumTag] = MISSING,
|
||||
poll: Poll = MISSING,
|
||||
view: View = MISSING,
|
||||
view: BaseView = MISSING,
|
||||
) -> Optional[SyncWebhookMessage]:
|
||||
"""Sends a message using the webhook.
|
||||
|
||||
@ -994,8 +1034,8 @@ class SyncWebhook(BaseWebhook):
|
||||
When sending a Poll via webhook, you cannot manually end it.
|
||||
|
||||
.. versionadded:: 2.4
|
||||
view: :class:`~discord.ui.View`
|
||||
The view to send with the message. This can only have URL buttons, which donnot
|
||||
view: Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]
|
||||
The view to send with the message. This can only have non-interactible items, which do not
|
||||
require a state to be attached to it.
|
||||
|
||||
If you want to send a view with any component attached to it, check :meth:`Webhook.send`.
|
||||
@ -1143,6 +1183,33 @@ class SyncWebhook(BaseWebhook):
|
||||
)
|
||||
return self._create_message(data, thread=thread)
|
||||
|
||||
@overload
|
||||
def edit_message(
|
||||
self,
|
||||
message_id: int,
|
||||
*,
|
||||
attachments: Sequence[Union[Attachment, File]] = ...,
|
||||
view: LayoutView,
|
||||
allowed_mentions: Optional[AllowedMentions] = ...,
|
||||
thread: Snowflake = ...,
|
||||
) -> SyncWebhookMessage:
|
||||
...
|
||||
|
||||
@overload
|
||||
def edit_message(
|
||||
self,
|
||||
message_id: int,
|
||||
*,
|
||||
content: Optional[str] = ...,
|
||||
embeds: Sequence[Embed] = ...,
|
||||
embed: Optional[Embed] = ...,
|
||||
attachments: Sequence[Union[Attachment, File]] = ...,
|
||||
view: Optional[View] = ...,
|
||||
allowed_mentions: Optional[AllowedMentions] = ...,
|
||||
thread: Snowflake = ...,
|
||||
) -> SyncWebhookMessage:
|
||||
...
|
||||
|
||||
def edit_message(
|
||||
self,
|
||||
message_id: int,
|
||||
@ -1151,6 +1218,7 @@ class SyncWebhook(BaseWebhook):
|
||||
embeds: Sequence[Embed] = MISSING,
|
||||
embed: Optional[Embed] = MISSING,
|
||||
attachments: Sequence[Union[Attachment, File]] = MISSING,
|
||||
view: Optional[BaseView] = MISSING,
|
||||
allowed_mentions: Optional[AllowedMentions] = None,
|
||||
thread: Snowflake = MISSING,
|
||||
) -> SyncWebhookMessage:
|
||||
@ -1177,6 +1245,13 @@ class SyncWebhook(BaseWebhook):
|
||||
then all attachments are removed.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]]
|
||||
The updated view to update this message with. This can only have non-interactible items, which do not
|
||||
require a state to be attached to it. If ``None`` is passed then the view is removed.
|
||||
|
||||
If you want to edit a webhook message with any component attached to it, check :meth:`WebhookMessage.edit`.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
allowed_mentions: :class:`AllowedMentions`
|
||||
Controls the mentions being processed in this message.
|
||||
See :meth:`.abc.Messageable.send` for more information.
|
||||
|
50
docs/api.rst
50
docs/api.rst
@ -4044,6 +4044,27 @@ of :class:`enum.Enum`.
|
||||
Default channels and questions count towards onboarding constraints.
|
||||
|
||||
|
||||
|
||||
.. class:: MediaItemLoadingState
|
||||
|
||||
Represents a :class:`UnfurledMediaItem` load state.
|
||||
|
||||
.. attribute:: unknown
|
||||
|
||||
Unknown load state.
|
||||
|
||||
.. attribute:: loading
|
||||
|
||||
The media item is still loading.
|
||||
|
||||
.. attribute:: loaded
|
||||
|
||||
The media item is loaded.
|
||||
|
||||
.. attribute:: not_found
|
||||
|
||||
The media item was not found.
|
||||
|
||||
.. _discord-api-audit-logs:
|
||||
|
||||
Audit Log Data
|
||||
@ -5716,8 +5737,6 @@ PollAnswer
|
||||
.. autoclass:: PollAnswer()
|
||||
:members:
|
||||
|
||||
.. _discord_api_data:
|
||||
|
||||
MessageSnapshot
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -5742,6 +5761,16 @@ PrimaryGuild
|
||||
.. autoclass:: PrimaryGuild()
|
||||
:members:
|
||||
|
||||
CallMessage
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: CallMessage
|
||||
|
||||
.. autoclass:: CallMessage()
|
||||
:members:
|
||||
|
||||
.. _discord_api_data:
|
||||
|
||||
Data Classes
|
||||
--------------
|
||||
|
||||
@ -6053,12 +6082,21 @@ PollMedia
|
||||
.. autoclass:: PollMedia
|
||||
:members:
|
||||
|
||||
CallMessage
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
UnfurledMediaItem
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: CallMessage
|
||||
.. attributetable:: UnfurledMediaItem
|
||||
|
||||
.. autoclass:: CallMessage()
|
||||
.. autoclass:: UnfurledMediaItem
|
||||
:members:
|
||||
|
||||
|
||||
MediaGalleryItem
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: MediaGalleryItem
|
||||
|
||||
.. autoclass:: MediaGalleryItem
|
||||
:members:
|
||||
|
||||
|
||||
|
@ -113,6 +113,77 @@ TextInput
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
SectionComponent
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: SectionComponent
|
||||
|
||||
.. autoclass:: SectionComponent()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
ThumbnailComponent
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: ThumbnailComponent
|
||||
|
||||
.. autoclass:: ThumbnailComponent()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
TextDisplay
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: TextDisplay
|
||||
|
||||
.. autoclass:: TextDisplay()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
MediaGalleryComponent
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: MediaGalleryComponent
|
||||
|
||||
.. autoclass:: MediaGalleryComponent()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
FileComponent
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: FileComponent
|
||||
|
||||
.. autoclass:: FileComponent()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
SeparatorComponent
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: SeparatorComponent
|
||||
|
||||
.. autoclass:: SeparatorComponent()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Container
|
||||
~~~~~~~~~
|
||||
|
||||
.. attributetable:: Container
|
||||
|
||||
.. autoclass:: Container()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
AppCommand
|
||||
~~~~~~~~~~~
|
||||
|
||||
@ -299,7 +370,7 @@ Enumerations
|
||||
|
||||
.. attribute:: action_row
|
||||
|
||||
Represents the group component which holds different components in a row.
|
||||
Represents a component which holds different components in a row.
|
||||
|
||||
.. attribute:: button
|
||||
|
||||
@ -333,6 +404,48 @@ Enumerations
|
||||
|
||||
Represents a channel select component.
|
||||
|
||||
.. attribute:: section
|
||||
|
||||
Represents a component which holds different components in a section.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. attribute:: text_display
|
||||
|
||||
Represents a text display component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. attribute:: thumbnail
|
||||
|
||||
Represents a thumbnail component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. attribute:: media_gallery
|
||||
|
||||
Represents a media gallery component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. attribute:: file
|
||||
|
||||
Represents a file component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. attribute:: separator
|
||||
|
||||
Represents a separator component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. attribute:: container
|
||||
|
||||
Represents a component which holds different components in a container.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. class:: ButtonStyle
|
||||
|
||||
Represents the style of the button component.
|
||||
@ -467,6 +580,19 @@ Enumerations
|
||||
|
||||
The permission is for a user.
|
||||
|
||||
.. class:: SeparatorSpacing
|
||||
|
||||
The separator's size type.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. attribute:: small
|
||||
|
||||
A small separator.
|
||||
.. attribute:: large
|
||||
|
||||
A large separator.
|
||||
|
||||
.. _discord_ui_kit:
|
||||
|
||||
Bot UI Kit
|
||||
@ -482,6 +608,16 @@ View
|
||||
|
||||
.. autoclass:: discord.ui.View
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
LayoutView
|
||||
~~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.LayoutView
|
||||
|
||||
.. autoclass:: discord.ui.LayoutView
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
Modal
|
||||
~~~~~~
|
||||
@ -586,6 +722,86 @@ TextInput
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Container
|
||||
~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.Container
|
||||
|
||||
.. autoclass:: discord.ui.Container
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
File
|
||||
~~~~
|
||||
|
||||
.. attributetable:: discord.ui.File
|
||||
|
||||
.. autoclass:: discord.ui.File
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
MediaGallery
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.MediaGallery
|
||||
|
||||
.. autoclass:: discord.ui.MediaGallery
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Section
|
||||
~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.Section
|
||||
|
||||
.. autoclass:: discord.ui.Section
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Separator
|
||||
~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.Separator
|
||||
|
||||
.. autoclass:: discord.ui.Separator
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
TextDisplay
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.TextDisplay
|
||||
|
||||
.. autoclass:: discord.ui.TextDisplay
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
Thumbnail
|
||||
~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.Thumbnail
|
||||
|
||||
.. autoclass:: discord.ui.Thumbnail
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
ActionRow
|
||||
~~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.ActionRow
|
||||
|
||||
.. autoclass:: discord.ui.ActionRow
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
.. _discord_app_commands:
|
||||
|
||||
Application Commands
|
||||
|
47
examples/views/layout.py
Normal file
47
examples/views/layout.py
Normal file
@ -0,0 +1,47 @@
|
||||
# This example requires the 'message_content' privileged intent to function.
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
class Bot(commands.Bot):
|
||||
def __init__(self):
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('$'), intents=intents)
|
||||
|
||||
async def on_ready(self):
|
||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||
print('------')
|
||||
|
||||
|
||||
# Define a LayoutView, which will allow us to add v2 components to it.
|
||||
class Layout(discord.ui.LayoutView):
|
||||
# you can add any top-level component (ui.ActionRow, ui.Section, ui.Container, ui.File, etc.) here
|
||||
|
||||
action_row = discord.ui.ActionRow()
|
||||
|
||||
@action_row.button(label='Click Me!')
|
||||
async def action_row_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.send_message('Hi!', ephemeral=True)
|
||||
|
||||
container = discord.ui.Container(
|
||||
discord.ui.TextDisplay(
|
||||
'Click the above button to receive a **very special** message!',
|
||||
),
|
||||
accent_colour=discord.Colour.blurple(),
|
||||
)
|
||||
|
||||
|
||||
bot = Bot()
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def layout(ctx: commands.Context):
|
||||
"""Sends a very special message!"""
|
||||
await ctx.send(view=Layout()) # sending LayoutView's does not allow for sending any content, embed(s), stickers, or poll
|
||||
|
||||
|
||||
bot.run('token')
|
Loading…
x
Reference in New Issue
Block a user