mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-04-18 23:15:48 +00:00
Add support for Modal Interactions
This commit is contained in:
parent
964ca0a6f1
commit
19c6687b55
@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
|
||||
from .enums import try_enum, ComponentType, ButtonStyle
|
||||
from .enums import try_enum, ComponentType, ButtonStyle, TextStyle
|
||||
from .utils import get_slots, MISSING
|
||||
from .partial_emoji import PartialEmoji, _EmojiTag
|
||||
|
||||
@ -36,6 +36,7 @@ if TYPE_CHECKING:
|
||||
SelectMenu as SelectMenuPayload,
|
||||
SelectOption as SelectOptionPayload,
|
||||
ActionRow as ActionRowPayload,
|
||||
TextInput as TextInputPayload,
|
||||
)
|
||||
from .emoji import Emoji
|
||||
|
||||
@ -370,6 +371,83 @@ class SelectOption:
|
||||
return payload
|
||||
|
||||
|
||||
class TextInput(Component):
|
||||
"""Represents a text input from the Discord Bot UI Kit.
|
||||
|
||||
.. note::
|
||||
The user constructible and usable type to create a text input is
|
||||
:class:`discord.ui.TextInput` not this one.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the text input that gets received during an interaction.
|
||||
label: :class:`str`
|
||||
The label to display above the text input.
|
||||
style: :class:`TextStyle`
|
||||
The style of the text input.
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text to display when the text input is empty.
|
||||
default_value: Optional[:class:`str`]
|
||||
The default value of the text input.
|
||||
required: :class:`bool`
|
||||
Whether the text input is required.
|
||||
min_length: Optional[:class:`int`]
|
||||
The minimum length of the text input.
|
||||
max_length: Optional[:class:`int`]
|
||||
The maximum length of the text input.
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
'style',
|
||||
'label',
|
||||
'custom_id',
|
||||
'placeholder',
|
||||
'default_value',
|
||||
'required',
|
||||
'min_length',
|
||||
'max_length',
|
||||
)
|
||||
|
||||
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
|
||||
|
||||
def __init__(self, data: TextInputPayload) -> None:
|
||||
self.type: ComponentType = ComponentType.text_input
|
||||
self.style: TextStyle = try_enum(TextStyle, data['style'])
|
||||
self.label: str = data['label']
|
||||
self.custom_id: str = data['custom_id']
|
||||
self.placeholder: Optional[str] = data.get('placeholder')
|
||||
self.default_value: Optional[str] = data.get('value')
|
||||
self.required: bool = data.get('required', True)
|
||||
self.min_length: Optional[int] = data.get('min_length')
|
||||
self.max_length: Optional[int] = data.get('max_length')
|
||||
|
||||
def to_dict(self) -> TextInputPayload:
|
||||
payload: TextInputPayload = {
|
||||
'type': self.type.value,
|
||||
'style': self.style.value,
|
||||
'label': self.label,
|
||||
'custom_id': self.custom_id,
|
||||
'required': self.required,
|
||||
}
|
||||
|
||||
if self.placeholder:
|
||||
payload['placeholder'] = self.placeholder
|
||||
|
||||
if self.default_value:
|
||||
payload['value'] = self.default_value
|
||||
|
||||
if self.min_length:
|
||||
payload['min_length'] = self.min_length
|
||||
|
||||
if self.max_length:
|
||||
payload['max_length'] = self.max_length
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def _component_factory(data: ComponentPayload) -> Component:
|
||||
component_type = data['type']
|
||||
if component_type == 1:
|
||||
@ -378,6 +456,8 @@ def _component_factory(data: ComponentPayload) -> Component:
|
||||
return Button(data) # type: ignore
|
||||
elif component_type == 3:
|
||||
return SelectMenu(data) # type: ignore
|
||||
elif component_type == 4:
|
||||
return TextInput(data) # type: ignore
|
||||
else:
|
||||
as_enum = try_enum(ComponentType, component_type)
|
||||
return Component._raw_construct(type=as_enum)
|
||||
|
@ -51,6 +51,7 @@ __all__ = (
|
||||
'VideoQualityMode',
|
||||
'ComponentType',
|
||||
'ButtonStyle',
|
||||
'TextStyle',
|
||||
'StagePrivacyLevel',
|
||||
'InteractionType',
|
||||
'InteractionResponseType',
|
||||
@ -530,6 +531,7 @@ class InteractionType(Enum):
|
||||
ping = 1
|
||||
application_command = 2
|
||||
component = 3
|
||||
modal_submit = 5
|
||||
|
||||
|
||||
class InteractionResponseType(Enum):
|
||||
@ -540,6 +542,7 @@ class InteractionResponseType(Enum):
|
||||
deferred_channel_message = 5 # (with source)
|
||||
deferred_message_update = 6 # for components
|
||||
message_update = 7 # for components
|
||||
modal = 9 # for modals
|
||||
|
||||
|
||||
class VideoQualityMode(Enum):
|
||||
@ -554,6 +557,7 @@ class ComponentType(Enum):
|
||||
action_row = 1
|
||||
button = 2
|
||||
select = 3
|
||||
text_input = 4
|
||||
|
||||
def __int__(self):
|
||||
return self.value
|
||||
@ -578,6 +582,17 @@ class ButtonStyle(Enum):
|
||||
return self.value
|
||||
|
||||
|
||||
class TextStyle(Enum):
|
||||
short = 1
|
||||
paragraph = 2
|
||||
|
||||
# Aliases
|
||||
long = 2
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
|
||||
|
||||
class StagePrivacyLevel(Enum):
|
||||
public = 1
|
||||
closed = 2
|
||||
|
@ -60,6 +60,7 @@ if TYPE_CHECKING:
|
||||
from aiohttp import ClientSession
|
||||
from .embeds import Embed
|
||||
from .ui.view import View
|
||||
from .ui.modal import Modal
|
||||
from .channel import VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, PartialMessageable
|
||||
from .threads import Thread
|
||||
|
||||
@ -628,6 +629,41 @@ class InteractionResponse:
|
||||
|
||||
self._responded = True
|
||||
|
||||
async def send_modal(self, modal: Modal, /):
|
||||
"""|coro|
|
||||
|
||||
Responds to this interaction by sending a modal.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
modal: :class:`~discord.ui.Modal`
|
||||
The modal to send.
|
||||
|
||||
Raises
|
||||
-------
|
||||
HTTPException
|
||||
Sending the modal failed.
|
||||
InteractionResponded
|
||||
This interaction has already been responded to before.
|
||||
"""
|
||||
if self._responded:
|
||||
raise InteractionResponded(self._parent)
|
||||
|
||||
parent = self._parent
|
||||
|
||||
adapter = async_context.get()
|
||||
|
||||
params = interaction_response_params(InteractionResponseType.modal.value, modal.to_dict())
|
||||
await adapter.create_interaction_response(
|
||||
parent.id,
|
||||
parent.token,
|
||||
session=parent._session,
|
||||
params=params,
|
||||
)
|
||||
|
||||
self._parent._state.store_view(modal)
|
||||
self._responded = True
|
||||
|
||||
|
||||
class _InteractionMessageState:
|
||||
__slots__ = ('_parent', '_interaction')
|
||||
|
@ -688,8 +688,11 @@ class ConnectionState:
|
||||
if data['type'] == 3: # interaction component
|
||||
custom_id = interaction.data['custom_id'] # type: ignore
|
||||
component_type = interaction.data['component_type'] # type: ignore
|
||||
self._view_store.dispatch(component_type, custom_id, interaction)
|
||||
|
||||
self._view_store.dispatch_view(component_type, custom_id, interaction)
|
||||
elif data['type'] == 5: # modal submit
|
||||
custom_id = interaction.data['custom_id'] # type: ignore
|
||||
components = interaction.data['components'] # type: ignore
|
||||
self._view_store.dispatch_modal(custom_id, interaction, components) # type: ignore
|
||||
self.dispatch('interaction', interaction)
|
||||
|
||||
def parse_presence_update(self, data) -> None:
|
||||
|
@ -29,6 +29,7 @@ from .emoji import PartialEmoji
|
||||
|
||||
ComponentType = Literal[1, 2, 3]
|
||||
ButtonStyle = Literal[1, 2, 3, 4, 5]
|
||||
TextStyle = Literal[1, 2]
|
||||
|
||||
|
||||
class ActionRow(TypedDict):
|
||||
@ -73,4 +74,19 @@ class SelectMenu(_SelectMenuOptional):
|
||||
options: List[SelectOption]
|
||||
|
||||
|
||||
Component = Union[ActionRow, ButtonComponent, SelectMenu]
|
||||
class _TextInputOptional(TypedDict, total=False):
|
||||
placeholder: str
|
||||
value: str
|
||||
required: bool
|
||||
min_length: int
|
||||
max_length: int
|
||||
|
||||
|
||||
class TextInput(_TextInputOptional):
|
||||
type: Literal[4]
|
||||
custom_id: str
|
||||
style: TextStyle
|
||||
label: str
|
||||
|
||||
|
||||
Component = Union[ActionRow, ButtonComponent, SelectMenu, TextInput]
|
||||
|
@ -163,13 +163,13 @@ class SelectMessageComponentInteractionData(_BaseMessageComponentInteractionData
|
||||
MessageComponentInteractionData = Union[ButtonMessageComponentInteractionData, SelectMessageComponentInteractionData]
|
||||
|
||||
|
||||
class ModalSubmitInputTextInteractionData(TypedDict):
|
||||
class ModalSubmitTextInputInteractionData(TypedDict):
|
||||
type: Literal[4]
|
||||
custom_id: str
|
||||
value: str
|
||||
|
||||
|
||||
ModalSubmitComponentItemInteractionData = ModalSubmitInputTextInteractionData
|
||||
ModalSubmitComponentItemInteractionData = ModalSubmitTextInputInteractionData
|
||||
|
||||
|
||||
class ModalSubmitActionRowInteractionData(TypedDict):
|
||||
|
@ -10,6 +10,8 @@ Bot UI Kit helper for the Discord API
|
||||
"""
|
||||
|
||||
from .view import *
|
||||
from .modal import *
|
||||
from .item import *
|
||||
from .button import *
|
||||
from .select import *
|
||||
from .text_input import *
|
||||
|
@ -73,7 +73,7 @@ class Item(Generic[V]):
|
||||
def refresh_component(self, component: Component) -> None:
|
||||
return None
|
||||
|
||||
def refresh_state(self, interaction: Interaction) -> None:
|
||||
def refresh_state(self, data: Dict[str, Any]) -> None:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
|
196
discord/ui/modal.py
Normal file
196
discord/ui/modal.py
Normal file
@ -0,0 +1,196 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, ClassVar, List
|
||||
|
||||
from ..utils import MISSING, find
|
||||
from .item import Item
|
||||
from .view import View
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from ..types.interactions import ModalSubmitComponentInteractionData as ModalSubmitComponentInteractionDataPayload
|
||||
|
||||
|
||||
__all__ = (
|
||||
'Modal',
|
||||
)
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Modal(View):
|
||||
"""Represents a UI modal.
|
||||
|
||||
This object must be inherited to create a modal popup window within discord.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
title: :class:`str`
|
||||
The title of the modal.
|
||||
timeout: Optional[:class:`float`]
|
||||
Timeout in seconds from last interaction with the UI before no longer accepting input.
|
||||
If ``None`` then there is no timeout.
|
||||
custom_id: :class:`str`
|
||||
The ID of the modal that gets received during an interaction.
|
||||
If not given then one is generated for you.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
timeout: Optional[:class:`float`]
|
||||
Timeout from last interaction with the UI before no longer accepting input.
|
||||
If ``None`` then there is no timeout.
|
||||
title: :class:`str`
|
||||
The title of the modal.
|
||||
children: List[:class:`Item`]
|
||||
The list of children attached to this view.
|
||||
custom_id: :class:`str`
|
||||
The ID of the modal that gets received during an interaction.
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
title: str
|
||||
|
||||
__discord_ui_modal__ = True
|
||||
__modal_children_items__: ClassVar[Dict[str, Item]] = {}
|
||||
|
||||
def __init_subclass__(cls, *, title: str = MISSING) -> None:
|
||||
if title is not MISSING:
|
||||
cls.title = title
|
||||
|
||||
children = {}
|
||||
for base in reversed(cls.__mro__):
|
||||
for name, member in base.__dict__.items():
|
||||
if isinstance(member, Item):
|
||||
children[name] = member
|
||||
|
||||
cls.__modal_children_items__ = children
|
||||
|
||||
def _init_children(self) -> List[Item]:
|
||||
children = []
|
||||
for name, item in self.__modal_children_items__.items():
|
||||
item = deepcopy(item)
|
||||
setattr(self, name, item)
|
||||
item._view = self
|
||||
children.append(item)
|
||||
return children
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
title: str = MISSING,
|
||||
timeout: Optional[float] = None,
|
||||
custom_id: str = MISSING,
|
||||
) -> None:
|
||||
if title is MISSING and getattr(self, 'title', MISSING) is MISSING:
|
||||
raise ValueError('Modal must have a title')
|
||||
elif title is not MISSING:
|
||||
self.title = title
|
||||
self.custom_id: str = os.urandom(16).hex() if custom_id is MISSING else custom_id
|
||||
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
async def on_submit(self, interaction: Interaction):
|
||||
"""|coro|
|
||||
|
||||
Called when the modal is submitted.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
interaction: :class:`.Interaction`
|
||||
The interaction that submitted this modal.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def on_error(self, error: Exception, interaction: Interaction) -> None:
|
||||
"""|coro|
|
||||
|
||||
A callback that is called when :meth:`on_submit`
|
||||
fails with an error.
|
||||
|
||||
The default implementation prints the traceback to stderr.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
error: :class:`Exception`
|
||||
The exception that was raised.
|
||||
interaction: :class:`~discord.Interaction`
|
||||
The interaction that led to the failure.
|
||||
"""
|
||||
print(f'Ignoring exception in modal {self}:', file=sys.stderr)
|
||||
traceback.print_exception(error.__class__, error, error.__traceback__, file=sys.stderr)
|
||||
|
||||
def refresh(self, components: Sequence[ModalSubmitComponentInteractionDataPayload]):
|
||||
for component in components:
|
||||
if component['type'] == 1:
|
||||
self.refresh(component['components'])
|
||||
else:
|
||||
item = find(lambda i: i.custom_id == component['custom_id'], self.children) # type: ignore
|
||||
if item is None:
|
||||
_log.debug("Modal interaction referencing unknown item custom_id %s. Discarding", component['custom_id'])
|
||||
continue
|
||||
item.refresh_state(component) # type: ignore
|
||||
|
||||
async def _scheduled_task(self, interaction: Interaction):
|
||||
try:
|
||||
if self.timeout:
|
||||
self.__timeout_expiry = time.monotonic() + self.timeout
|
||||
|
||||
allow = await self.interaction_check(interaction)
|
||||
if not allow:
|
||||
return
|
||||
|
||||
await self.on_submit(interaction)
|
||||
if not interaction.response._responded:
|
||||
await interaction.response.defer()
|
||||
except Exception as e:
|
||||
return await self.on_error(e, interaction)
|
||||
else:
|
||||
# No error, so assume this will always happen
|
||||
# In the future, maybe this will require checking if we set an error response.
|
||||
self.stop()
|
||||
|
||||
def _dispatch_submit(self, interaction: Interaction) -> None:
|
||||
asyncio.create_task(self._scheduled_task(interaction), name=f'discord-ui-modal-dispatch-{self.id}')
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
payload = {
|
||||
'custom_id': self.custom_id,
|
||||
'title': self.title,
|
||||
'components': self.to_components(),
|
||||
}
|
||||
|
||||
return payload
|
@ -47,7 +47,7 @@ if TYPE_CHECKING:
|
||||
from .view import View
|
||||
from ..types.components import SelectMenu as SelectMenuPayload
|
||||
from ..types.interactions import (
|
||||
ComponentInteractionData,
|
||||
MessageComponentInteractionData,
|
||||
)
|
||||
|
||||
S = TypeVar('S', bound='Select')
|
||||
@ -270,8 +270,7 @@ class Select(Item[V]):
|
||||
def refresh_component(self, component: SelectMenu) -> None:
|
||||
self._underlying = component
|
||||
|
||||
def refresh_state(self, interaction: Interaction) -> None:
|
||||
data: ComponentInteractionData = interaction.data # type: ignore
|
||||
def refresh_state(self, data: MessageComponentInteractionData) -> None:
|
||||
self._selected_values = data.get('values', [])
|
||||
|
||||
@classmethod
|
||||
|
231
discord/ui/text_input.py
Normal file
231
discord/ui/text_input.py
Normal file
@ -0,0 +1,231 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, TypeVar
|
||||
|
||||
from ..components import TextInput as TextInputComponent
|
||||
from ..enums import ComponentType, TextStyle
|
||||
from ..utils import MISSING
|
||||
from .item import Item
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..types.components import TextInput as TextInputPayload
|
||||
from ..types.interactions import ModalSubmitTextInputInteractionData as ModalSubmitTextInputInteractionDataPayload
|
||||
from .view import View
|
||||
|
||||
|
||||
__all__ = (
|
||||
'TextInput',
|
||||
)
|
||||
|
||||
|
||||
V = TypeVar('V', bound='View', covariant=True)
|
||||
|
||||
|
||||
class TextInput(Item[V]):
|
||||
"""Represents a UI text input.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
------------
|
||||
label: :class:`str`
|
||||
The label to display above the text input.
|
||||
custom_id: :class:`str`
|
||||
The ID of the text input that gets recieved during an interaction.
|
||||
If not given then one is generated for you.
|
||||
style: :class:`discord.TextStyle`
|
||||
The style of the text input.
|
||||
placeholder: Optional[:class:`str`]
|
||||
The placeholder text to display when the text input is empty.
|
||||
default_value: Optional[:class:`str`]
|
||||
The default value of the text input.
|
||||
required: :class:`bool`
|
||||
Whether the text input is required.
|
||||
min_length: Optional[:class:`int`]
|
||||
The minimum length of the text input.
|
||||
max_length: Optional[:class:`int`]
|
||||
The maximum length of the text input.
|
||||
row: Optional[:class:`int`]
|
||||
The relative row this text input belongs to. A Discord component can only have 5
|
||||
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||
like to control the relative positioning of the row then passing an index is advised.
|
||||
For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
|
||||
ordering. The row number must be between 0 and 4 (i.e. zero indexed).
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: Tuple[str, ...] = (
|
||||
'label',
|
||||
'placeholder',
|
||||
'required',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
label: str,
|
||||
style: TextStyle = TextStyle.short,
|
||||
custom_id: str = MISSING,
|
||||
placeholder: Optional[str] = None,
|
||||
default_value: Optional[str] = None,
|
||||
required: bool = True,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
row: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._value: Optional[str] = default_value
|
||||
self._provided_custom_id = custom_id is not MISSING
|
||||
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
|
||||
self._underlying = TextInputComponent._raw_construct(
|
||||
type=ComponentType.text_input,
|
||||
label=label,
|
||||
style=style,
|
||||
custom_id=custom_id,
|
||||
placeholder=placeholder,
|
||||
default_value=default_value,
|
||||
required=required,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
)
|
||||
self.row: Optional[int] = row
|
||||
|
||||
@property
|
||||
def custom_id(self) -> str:
|
||||
""":class:`str`: The ID of the select menu that gets received during an interaction."""
|
||||
return self._underlying.custom_id
|
||||
|
||||
@custom_id.setter
|
||||
def custom_id(self, value: str) -> None:
|
||||
if not isinstance(value, str):
|
||||
raise TypeError('custom_id must be None or str')
|
||||
|
||||
self._underlying.custom_id = value
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return 5
|
||||
|
||||
@property
|
||||
def value(self) -> Optional[str]:
|
||||
"""Optional[:class:`str`]: The value of the text input."""
|
||||
return self._value
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
""":class:`str`: The label of the text input."""
|
||||
return self._underlying.label
|
||||
|
||||
@label.setter
|
||||
def label(self, value: str) -> None:
|
||||
self._underlying.label = value
|
||||
|
||||
@property
|
||||
def placeholder(self) -> Optional[str]:
|
||||
""":class:`str`: The placeholder text to display when the text input is empty."""
|
||||
return self._underlying.placeholder
|
||||
|
||||
@placeholder.setter
|
||||
def placeholder(self, value: Optional[str]) -> None:
|
||||
self._underlying.placeholder = value
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
""":class:`bool`: Whether the text input is required."""
|
||||
return self._underlying.required
|
||||
|
||||
@required.setter
|
||||
def required(self, value: bool) -> None:
|
||||
self._underlying.required = value
|
||||
|
||||
@property
|
||||
def min_length(self) -> Optional[int]:
|
||||
""":class:`int`: The minimum length of the text input."""
|
||||
return self._underlying.min_length
|
||||
|
||||
@min_length.setter
|
||||
def min_length(self, value: Optional[int]) -> None:
|
||||
self._underlying.min_length = value
|
||||
|
||||
@property
|
||||
def max_length(self) -> Optional[int]:
|
||||
""":class:`int`: The maximum length of the text input."""
|
||||
return self._underlying.max_length
|
||||
|
||||
@max_length.setter
|
||||
def max_length(self, value: Optional[int]) -> None:
|
||||
self._underlying.max_length = value
|
||||
|
||||
@property
|
||||
def style(self) -> TextStyle:
|
||||
""":class:`discord.TextStyle`: The style of the text input."""
|
||||
return self._underlying.style
|
||||
|
||||
@style.setter
|
||||
def style(self, value: TextStyle) -> None:
|
||||
self._underlying.style = value
|
||||
|
||||
@property
|
||||
def default_value(self) -> Optional[str]:
|
||||
""":class:`str`: The default value of the text input."""
|
||||
return self._underlying.default_value
|
||||
|
||||
@default_value.setter
|
||||
def default_value(self, value: Optional[str]) -> None:
|
||||
self._underlying.default_value = value
|
||||
|
||||
def to_component_dict(self) -> TextInputPayload:
|
||||
return self._underlying.to_dict()
|
||||
|
||||
def refresh_component(self, component: TextInputComponent) -> None:
|
||||
self._underlying = component
|
||||
|
||||
def refresh_state(self, data: ModalSubmitTextInputInteractionDataPayload) -> None:
|
||||
self._value = data.get('value', None)
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: TextInput) -> Self:
|
||||
return cls(
|
||||
label=component.label,
|
||||
style=component.style,
|
||||
custom_id=component.custom_id,
|
||||
placeholder=component.placeholder,
|
||||
default_value=component.default_value,
|
||||
required=component.required,
|
||||
min_length=component.min_length,
|
||||
max_length=component.max_length,
|
||||
row=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def type(self) -> ComponentType:
|
||||
return ComponentType.text_input
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return False
|
@ -29,6 +29,7 @@ from itertools import groupby
|
||||
|
||||
import traceback
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
@ -40,6 +41,7 @@ from ..components import (
|
||||
Button as ButtonComponent,
|
||||
SelectMenu as SelectComponent,
|
||||
)
|
||||
from ..utils import MISSING
|
||||
|
||||
__all__ = (
|
||||
'View',
|
||||
@ -50,7 +52,12 @@ if TYPE_CHECKING:
|
||||
from ..interactions import Interaction
|
||||
from ..message import Message
|
||||
from ..types.components import Component as ComponentPayload
|
||||
from ..types.interactions import ModalSubmitComponentInteractionData as ModalSubmitComponentInteractionDataPayload
|
||||
from ..state import ConnectionState
|
||||
from .modal import Modal
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _walk_all_components(components: List[Component]) -> Iterator[Component]:
|
||||
@ -138,6 +145,7 @@ class View:
|
||||
"""
|
||||
|
||||
__discord_ui_view__: ClassVar[bool] = True
|
||||
__discord_ui_modal__: ClassVar[bool] = False
|
||||
__view_children_items__: ClassVar[List[ItemCallbackType]] = []
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
@ -152,16 +160,19 @@ class View:
|
||||
|
||||
cls.__view_children_items__ = children
|
||||
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0):
|
||||
self.timeout = timeout
|
||||
self.children: List[Item] = []
|
||||
def _init_children(self) -> List[Item]:
|
||||
children = []
|
||||
for func in self.__view_children_items__:
|
||||
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
|
||||
item.callback = partial(func, self, item)
|
||||
item.callback = partial(func, self, item) # type: ignore
|
||||
item._view = self
|
||||
setattr(self, func.__name__, item)
|
||||
self.children.append(item)
|
||||
children.append(item)
|
||||
return children
|
||||
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0):
|
||||
self.timeout = timeout
|
||||
self.children: List[Item] = self._init_children()
|
||||
self.__weights = _ViewWeights(self.children)
|
||||
loop = asyncio.get_running_loop()
|
||||
self.id: str = os.urandom(16).hex()
|
||||
@ -464,6 +475,8 @@ class ViewStore:
|
||||
self._views: Dict[Tuple[int, Optional[int], str], Tuple[View, Item]] = {}
|
||||
# message_id: View
|
||||
self._synced_message_views: Dict[int, View] = {}
|
||||
# custom_id: Modal
|
||||
self._modals: Dict[str, Modal] = {}
|
||||
self._state: ConnectionState = state
|
||||
|
||||
@property
|
||||
@ -487,6 +500,10 @@ class ViewStore:
|
||||
del self._views[k]
|
||||
|
||||
def add_view(self, view: View, message_id: Optional[int] = None):
|
||||
if view.__discord_ui_modal__:
|
||||
self._modals[view.custom_id] = view # type: ignore
|
||||
return
|
||||
|
||||
self.__verify_integrity()
|
||||
|
||||
view._start_listening_from_store(self)
|
||||
@ -498,6 +515,10 @@ class ViewStore:
|
||||
self._synced_message_views[message_id] = view
|
||||
|
||||
def remove_view(self, view: View):
|
||||
if view.__discord_ui_modal__:
|
||||
self._modals.pop(view.custom_id, None) # type: ignore
|
||||
return
|
||||
|
||||
for item in view.children:
|
||||
if item.is_dispatchable():
|
||||
self._views.pop((item.type.value, item.custom_id), None) # type: ignore
|
||||
@ -507,7 +528,7 @@ class ViewStore:
|
||||
del self._synced_message_views[key]
|
||||
break
|
||||
|
||||
def dispatch(self, component_type: int, custom_id: str, interaction: Interaction):
|
||||
def dispatch_view(self, component_type: int, custom_id: str, interaction: Interaction):
|
||||
self.__verify_integrity()
|
||||
message_id: Optional[int] = interaction.message and interaction.message.id
|
||||
key = (component_type, message_id, custom_id)
|
||||
@ -518,9 +539,18 @@ class ViewStore:
|
||||
return
|
||||
|
||||
view, item = value
|
||||
item.refresh_state(interaction)
|
||||
item.refresh_state(interaction.data) # type: ignore
|
||||
view._dispatch_item(item, interaction)
|
||||
|
||||
def dispatch_modal(self, custom_id: str, interaction: Interaction, components: List[ModalSubmitComponentInteractionDataPayload]):
|
||||
modal = self._modals.get(custom_id)
|
||||
if modal is None:
|
||||
_log.debug("Modal interaction referencing unknown custom_id %s. Discarding", custom_id)
|
||||
return
|
||||
|
||||
modal.refresh(components)
|
||||
modal._dispatch_submit(interaction)
|
||||
|
||||
def is_message_tracked(self, message_id: int):
|
||||
return message_id in self._synced_message_views
|
||||
|
||||
|
54
docs/api.rst
54
docs/api.rst
@ -1418,6 +1418,9 @@ of :class:`enum.Enum`.
|
||||
.. attribute:: component
|
||||
|
||||
Represents a component based interaction, i.e. using the Discord Bot UI Kit.
|
||||
.. attribute:: modal_submit
|
||||
|
||||
Represents submission of a modal interaction.
|
||||
|
||||
.. class:: InteractionResponseType
|
||||
|
||||
@ -1451,6 +1454,11 @@ of :class:`enum.Enum`.
|
||||
Responds to the interaction by editing the message.
|
||||
|
||||
See also :meth:`InteractionResponse.edit_message`
|
||||
.. attribute:: modal
|
||||
|
||||
Responds to the interaction with a modal.
|
||||
|
||||
See also :meth:`InteractionResponse.send_modal`
|
||||
|
||||
.. class:: ComponentType
|
||||
|
||||
@ -1467,6 +1475,10 @@ of :class:`enum.Enum`.
|
||||
.. attribute:: select
|
||||
|
||||
Represents a select component.
|
||||
|
||||
.. attribute:: text_input
|
||||
|
||||
Represents a text box component.
|
||||
|
||||
|
||||
.. class:: ButtonStyle
|
||||
@ -1510,6 +1522,22 @@ of :class:`enum.Enum`.
|
||||
|
||||
An alias for :attr:`link`.
|
||||
|
||||
.. class:: TextStyle
|
||||
|
||||
Represents the style of the text box component.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. attribute:: short
|
||||
|
||||
Represents a short text box.
|
||||
.. attribute:: paragraph
|
||||
|
||||
Represents a long form text box.
|
||||
.. attribute:: long
|
||||
|
||||
An alias for :attr:`paragraph`.
|
||||
|
||||
.. class:: VoiceRegion
|
||||
|
||||
Specifies the region a voice server belongs to.
|
||||
@ -3398,6 +3426,16 @@ SelectMenu
|
||||
:inherited-members:
|
||||
|
||||
|
||||
TextInput
|
||||
~~~~~~~~~~
|
||||
|
||||
.. attributetable:: TextInput
|
||||
|
||||
.. autoclass:: TextInput()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
DeletedReferencedMessage
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -4061,6 +4099,14 @@ View
|
||||
.. autoclass:: discord.ui.View
|
||||
:members:
|
||||
|
||||
Modal
|
||||
~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.Modal
|
||||
|
||||
.. autoclass:: discord.ui.Modal
|
||||
:members:
|
||||
|
||||
Item
|
||||
~~~~~~~
|
||||
|
||||
@ -4091,6 +4137,14 @@ Select
|
||||
|
||||
.. autofunction:: discord.ui.select
|
||||
|
||||
TextInput
|
||||
~~~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.TextInput
|
||||
|
||||
.. autoclass:: discord.ui.TextInput
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
Exceptions
|
||||
------------
|
||||
|
Loading…
x
Reference in New Issue
Block a user