Browse Source

Add support for Modal Interactions

pull/7494/head
Josh 3 years ago
committed by GitHub
parent
commit
19c6687b55
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 82
      discord/components.py
  2. 15
      discord/enums.py
  3. 36
      discord/interactions.py
  4. 7
      discord/state.py
  5. 18
      discord/types/components.py
  6. 4
      discord/types/interactions.py
  7. 2
      discord/ui/__init__.py
  8. 2
      discord/ui/item.py
  9. 196
      discord/ui/modal.py
  10. 5
      discord/ui/select.py
  11. 231
      discord/ui/text_input.py
  12. 44
      discord/ui/view.py
  13. 54
      docs/api.rst

82
discord/components.py

@ -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)

15
discord/enums.py

@ -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

36
discord/interactions.py

@ -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')

7
discord/state.py

@ -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:

18
discord/types/components.py

@ -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]

4
discord/types/interactions.py

@ -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):

2
discord/ui/__init__.py

@ -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 *

2
discord/ui/item.py

@ -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

@ -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

5
discord/ui/select.py

@ -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

@ -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

44
discord/ui/view.py

@ -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

@ -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…
Cancel
Save