Skip to content

Commit 8690471

Browse files
krittickLulalabyDorukyumMiddledot
authored
Add support for Input Text and Modal components (#630)
Co-authored-by: Lulalaby <[email protected]> Co-authored-by: Dorukyum <[email protected]> Co-authored-by: Middledot <[email protected]>
1 parent a428f0b commit 8690471

File tree

10 files changed

+534
-9
lines changed

10 files changed

+534
-9
lines changed

discord/bot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -730,8 +730,8 @@ async def process_application_commands(self, interaction: Interaction, auto_sync
730730
if auto_sync is None:
731731
auto_sync = self.auto_sync_commands
732732
if interaction.type not in (
733-
InteractionType.application_command,
734-
InteractionType.auto_complete
733+
InteractionType.application_command,
734+
InteractionType.auto_complete,
735735
):
736736
return
737737

discord/components.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@
2626
from __future__ import annotations
2727

2828
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
29-
from .enums import try_enum, ComponentType, ButtonStyle
29+
from .enums import try_enum, ComponentType, ButtonStyle, InputTextStyle
3030
from .utils import get_slots, MISSING
3131
from .partial_emoji import PartialEmoji, _EmojiTag
3232

3333
if TYPE_CHECKING:
3434
from .types.components import (
3535
Component as ComponentPayload,
36+
InputText as InputTextComponentPayload,
3637
ButtonComponent as ButtonComponentPayload,
3738
SelectMenu as SelectMenuPayload,
3839
SelectOption as SelectOptionPayload,
@@ -128,6 +129,82 @@ def to_dict(self) -> ActionRowPayload:
128129
} # type: ignore
129130

130131

132+
class InputText(Component):
133+
"""Represents an Input Text field from the Discord Bot UI Kit.
134+
This inherits from :class:`Component`.
135+
Attributes
136+
----------
137+
style: :class:`.InputTextStyle`
138+
The style of the input text field.
139+
custom_id: Optional[:class:`str`]
140+
The ID of the input text field that gets received during an interaction.
141+
label: Optional[:class:`str`]
142+
The label for the input text field, if any.
143+
placeholder: Optional[:class:`str`]
144+
The placeholder text that is shown if nothing is selected, if any.
145+
min_length: Optional[:class:`int`]
146+
The minimum number of characters that must be entered
147+
Defaults to 0
148+
max_length: Optional[:class:`int`]
149+
The maximum number of characters that can be entered
150+
required: Optional[:class:`bool`]
151+
Whether the input text field is required or not. Defaults to `True`.
152+
value: Optional[:class:`str`]
153+
The value that has been entered in the input text field.
154+
"""
155+
156+
__slots__: Tuple[str, ...] = (
157+
"type",
158+
"style",
159+
"custom_id",
160+
"label",
161+
"placeholder",
162+
"min_length",
163+
"max_length",
164+
"required",
165+
"value",
166+
)
167+
168+
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
169+
170+
def __init__(self, data: InputTextComponentPayload):
171+
self.type = ComponentType.input_text
172+
self.style: InputTextStyle = try_enum(InputTextStyle, data["style"])
173+
self.custom_id = data["custom_id"]
174+
self.label: Optional[str] = data.get("label", None)
175+
self.placeholder: Optional[str] = data.get("placeholder", None)
176+
self.min_length: Optional[int] = data.get("min_length", None)
177+
self.max_length: Optional[int] = data.get("max_length", None)
178+
self.required: bool = data.get("required", True)
179+
self.value: Optional[str] = data.get("value", None)
180+
181+
def to_dict(self) -> InputTextComponentPayload:
182+
payload = {
183+
"type": 4,
184+
"style": self.style.value,
185+
"label": self.label,
186+
}
187+
if self.custom_id:
188+
payload["custom_id"] = self.custom_id
189+
190+
if self.placeholder:
191+
payload["placeholder"] = self.placeholder
192+
193+
if self.min_length:
194+
payload["min_length"] = self.min_length
195+
196+
if self.max_length:
197+
payload["max_length"] = self.max_length
198+
199+
if not self.required:
200+
payload["required"] = self.required
201+
202+
if self.value:
203+
payload["value"] = self.value
204+
205+
return payload # type: ignore
206+
207+
131208
class Button(Component):
132209
"""Represents a button from the Discord Bot UI Kit.
133210

discord/enums.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
'ScheduledEventStatus',
6161
'ScheduledEventPrivacyLevel',
6262
'ScheduledEventLocationType',
63+
'InputTextStyle',
6364
)
6465

6566

@@ -550,6 +551,7 @@ class InteractionType(Enum):
550551
application_command = 2
551552
component = 3
552553
auto_complete = 4
554+
modal_submit = 5
553555

554556

555557
class InteractionResponseType(Enum):
@@ -561,7 +563,7 @@ class InteractionResponseType(Enum):
561563
deferred_message_update = 6 # for components
562564
message_update = 7 # for components
563565
auto_complete_result = 8 # for autocomplete interactions
564-
566+
modal = 9 # for modal dialogs
565567

566568
class VideoQualityMode(Enum):
567569
auto = 1
@@ -575,6 +577,7 @@ class ComponentType(Enum):
575577
action_row = 1
576578
button = 2
577579
select = 3
580+
input_text = 4
578581

579582
def __int__(self):
580583
return self.value
@@ -599,6 +602,14 @@ def __int__(self):
599602
return self.value
600603

601604

605+
class InputTextStyle(Enum):
606+
short = 1
607+
singleline = 1
608+
paragraph = 2
609+
multiline = 2
610+
long = 2
611+
612+
602613
class ApplicationType(Enum):
603614
game = 1
604615
music = 2

discord/interactions.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from aiohttp import ClientSession
6060
from .embeds import Embed
6161
from .ui.view import View
62+
from .ui.modal import Modal
6263
from .channel import VoiceChannel, StageChannel, TextChannel, CategoryChannel, StoreChannel, PartialMessageable
6364
from .threads import Thread
6465
from .commands import OptionChoice
@@ -775,7 +776,24 @@ async def send_autocomplete_result(
775776
)
776777

777778
self._responded = True
778-
779+
780+
async def send_modal(self, modal: Modal):
781+
if self._responded:
782+
raise InteractionResponded(self._parent)
783+
784+
payload = modal.to_dict()
785+
adapter = async_context.get()
786+
await adapter.create_interaction_response(
787+
self._parent.id,
788+
self._parent.token,
789+
session=self._parent._session,
790+
type=InteractionResponseType.modal.value,
791+
data=payload,
792+
)
793+
self._responded = True
794+
self._parent._state.store_modal(modal, self._parent.user.id)
795+
796+
779797
class _InteractionMessageState:
780798
__slots__ = ('_parent', '_interaction')
781799

discord/state.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,15 @@
4747
from .raw_models import *
4848
from .member import Member
4949
from .role import Role
50-
from .enums import ChannelType, try_enum, Status, ScheduledEventStatus
50+
from .enums import ChannelType, try_enum, Status, ScheduledEventStatus, InteractionType
5151
from . import utils
5252
from .flags import ApplicationFlags, Intents, MemberCacheFlags
5353
from .object import Object
5454
from .invite import Invite
5555
from .integrations import _integration_factory
5656
from .interactions import Interaction
5757
from .ui.view import ViewStore, View
58+
from .ui.modal import Modal, ModalStore
5859
from .stage_instance import StageInstance
5960
from .threads import Thread, ThreadMember
6061
from .sticker import GuildSticker
@@ -256,7 +257,7 @@ def clear(self, *, views: bool = True) -> None:
256257
self._guilds: Dict[int, Guild] = {}
257258
if views:
258259
self._view_store: ViewStore = ViewStore(self)
259-
260+
self._modal_store: ModalStore = ModalStore(self)
260261
self._voice_clients: Dict[int, VoiceProtocol] = {}
261262

262263
# LRU of max size 128
@@ -363,6 +364,9 @@ def store_sticker(self, guild: Guild, data: GuildStickerPayload) -> GuildSticker
363364
def store_view(self, view: View, message_id: Optional[int] = None) -> None:
364365
self._view_store.add_view(view, message_id)
365366

367+
def store_modal(self, modal: Modal, message_id: int) -> None:
368+
self._modal_store.add_modal(modal, message_id)
369+
366370
def prevent_view_updates_for(self, message_id: int) -> Optional[View]:
367371
return self._view_store.remove_message_tracking(message_id)
368372

@@ -705,6 +709,12 @@ def parse_interaction_create(self, data) -> None:
705709
custom_id = interaction.data['custom_id'] # type: ignore
706710
component_type = interaction.data['component_type'] # type: ignore
707711
self._view_store.dispatch(component_type, custom_id, interaction)
712+
if interaction.type == InteractionType.modal_submit:
713+
user_id, custom_id = (
714+
interaction.user.id,
715+
interaction.data["custom_id"],
716+
)
717+
asyncio.create_task(self._modal_store.dispatch(user_id, custom_id, interaction))
708718

709719
self.dispatch('interaction', interaction)
710720

discord/types/components.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
from typing import List, Literal, TypedDict, Union
2929
from .emoji import PartialEmoji
3030

31-
ComponentType = Literal[1, 2, 3]
31+
ComponentType = Literal[1, 2, 3, 4]
3232
ButtonStyle = Literal[1, 2, 3, 4, 5]
33+
InputTextStyle = Literal[1, 2]
3334

3435

3536
class ActionRow(TypedDict):
@@ -50,6 +51,21 @@ class ButtonComponent(_ButtonComponentOptional):
5051
style: ButtonStyle
5152

5253

54+
class _InputTextComponentOptional(TypedDict, total=False):
55+
min_length: int
56+
max_length: int
57+
required: bool
58+
placeholder: str
59+
value: str
60+
61+
62+
class InputText(_InputTextComponentOptional):
63+
type: Literal[4]
64+
style: InputTextStyle
65+
custom_id: str
66+
label: str
67+
68+
5369
class _SelectMenuOptional(TypedDict, total=False):
5470
placeholder: str
5571
min_values: int
@@ -74,4 +90,4 @@ class SelectMenu(_SelectMenuOptional):
7490
options: List[SelectOption]
7591

7692

77-
Component = Union[ActionRow, ButtonComponent, SelectMenu]
93+
Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText]

discord/ui/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313
from .item import *
1414
from .button import *
1515
from .select import *
16+
from .input_text import *
17+
from .modal import *

0 commit comments

Comments
 (0)