Skip to content

Commit

Permalink
Add support for Input Text and Modal components (Pycord-Development#630)
Browse files Browse the repository at this point in the history
Co-authored-by: Lulalaby <lala@pycord.dev>
Co-authored-by: Dorukyum <53639936+Dorukyum@users.noreply.github.com>
Co-authored-by: Middledot <78228142+Middledot@users.noreply.github.com>
  • Loading branch information
4 people authored Feb 9, 2022
1 parent a428f0b commit 8690471
Show file tree
Hide file tree
Showing 10 changed files with 534 additions and 9 deletions.
4 changes: 2 additions & 2 deletions discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,8 +730,8 @@ async def process_application_commands(self, interaction: Interaction, auto_sync
if auto_sync is None:
auto_sync = self.auto_sync_commands
if interaction.type not in (
InteractionType.application_command,
InteractionType.auto_complete
InteractionType.application_command,
InteractionType.auto_complete,
):
return

Expand Down
79 changes: 78 additions & 1 deletion discord/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
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, InputTextStyle
from .utils import get_slots, MISSING
from .partial_emoji import PartialEmoji, _EmojiTag

if TYPE_CHECKING:
from .types.components import (
Component as ComponentPayload,
InputText as InputTextComponentPayload,
ButtonComponent as ButtonComponentPayload,
SelectMenu as SelectMenuPayload,
SelectOption as SelectOptionPayload,
Expand Down Expand Up @@ -128,6 +129,82 @@ def to_dict(self) -> ActionRowPayload:
} # type: ignore


class InputText(Component):
"""Represents an Input Text field from the Discord Bot UI Kit.
This inherits from :class:`Component`.
Attributes
----------
style: :class:`.InputTextStyle`
The style of the input text field.
custom_id: Optional[:class:`str`]
The ID of the input text field that gets received during an interaction.
label: Optional[:class:`str`]
The label for the input text field, if any.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
min_length: Optional[:class:`int`]
The minimum number of characters that must be entered
Defaults to 0
max_length: Optional[:class:`int`]
The maximum number of characters that can be entered
required: Optional[:class:`bool`]
Whether the input text field is required or not. Defaults to `True`.
value: Optional[:class:`str`]
The value that has been entered in the input text field.
"""

__slots__: Tuple[str, ...] = (
"type",
"style",
"custom_id",
"label",
"placeholder",
"min_length",
"max_length",
"required",
"value",
)

__repr_info__: ClassVar[Tuple[str, ...]] = __slots__

def __init__(self, data: InputTextComponentPayload):
self.type = ComponentType.input_text
self.style: InputTextStyle = try_enum(InputTextStyle, data["style"])
self.custom_id = data["custom_id"]
self.label: Optional[str] = data.get("label", None)
self.placeholder: Optional[str] = data.get("placeholder", None)
self.min_length: Optional[int] = data.get("min_length", None)
self.max_length: Optional[int] = data.get("max_length", None)
self.required: bool = data.get("required", True)
self.value: Optional[str] = data.get("value", None)

def to_dict(self) -> InputTextComponentPayload:
payload = {
"type": 4,
"style": self.style.value,
"label": self.label,
}
if self.custom_id:
payload["custom_id"] = self.custom_id

if self.placeholder:
payload["placeholder"] = self.placeholder

if self.min_length:
payload["min_length"] = self.min_length

if self.max_length:
payload["max_length"] = self.max_length

if not self.required:
payload["required"] = self.required

if self.value:
payload["value"] = self.value

return payload # type: ignore


class Button(Component):
"""Represents a button from the Discord Bot UI Kit.
Expand Down
13 changes: 12 additions & 1 deletion discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
'ScheduledEventStatus',
'ScheduledEventPrivacyLevel',
'ScheduledEventLocationType',
'InputTextStyle',
)


Expand Down Expand Up @@ -550,6 +551,7 @@ class InteractionType(Enum):
application_command = 2
component = 3
auto_complete = 4
modal_submit = 5


class InteractionResponseType(Enum):
Expand All @@ -561,7 +563,7 @@ class InteractionResponseType(Enum):
deferred_message_update = 6 # for components
message_update = 7 # for components
auto_complete_result = 8 # for autocomplete interactions

modal = 9 # for modal dialogs

class VideoQualityMode(Enum):
auto = 1
Expand All @@ -575,6 +577,7 @@ class ComponentType(Enum):
action_row = 1
button = 2
select = 3
input_text = 4

def __int__(self):
return self.value
Expand All @@ -599,6 +602,14 @@ def __int__(self):
return self.value


class InputTextStyle(Enum):
short = 1
singleline = 1
paragraph = 2
multiline = 2
long = 2


class ApplicationType(Enum):
game = 1
music = 2
Expand Down
20 changes: 19 additions & 1 deletion discord/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
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
from .commands import OptionChoice
Expand Down Expand Up @@ -775,7 +776,24 @@ async def send_autocomplete_result(
)

self._responded = True


async def send_modal(self, modal: Modal):
if self._responded:
raise InteractionResponded(self._parent)

payload = modal.to_dict()
adapter = async_context.get()
await adapter.create_interaction_response(
self._parent.id,
self._parent.token,
session=self._parent._session,
type=InteractionResponseType.modal.value,
data=payload,
)
self._responded = True
self._parent._state.store_modal(modal, self._parent.user.id)


class _InteractionMessageState:
__slots__ = ('_parent', '_interaction')

Expand Down
14 changes: 12 additions & 2 deletions discord/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@
from .raw_models import *
from .member import Member
from .role import Role
from .enums import ChannelType, try_enum, Status, ScheduledEventStatus
from .enums import ChannelType, try_enum, Status, ScheduledEventStatus, InteractionType
from . import utils
from .flags import ApplicationFlags, Intents, MemberCacheFlags
from .object import Object
from .invite import Invite
from .integrations import _integration_factory
from .interactions import Interaction
from .ui.view import ViewStore, View
from .ui.modal import Modal, ModalStore
from .stage_instance import StageInstance
from .threads import Thread, ThreadMember
from .sticker import GuildSticker
Expand Down Expand Up @@ -256,7 +257,7 @@ def clear(self, *, views: bool = True) -> None:
self._guilds: Dict[int, Guild] = {}
if views:
self._view_store: ViewStore = ViewStore(self)

self._modal_store: ModalStore = ModalStore(self)
self._voice_clients: Dict[int, VoiceProtocol] = {}

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

def store_modal(self, modal: Modal, message_id: int) -> None:
self._modal_store.add_modal(modal, message_id)

def prevent_view_updates_for(self, message_id: int) -> Optional[View]:
return self._view_store.remove_message_tracking(message_id)

Expand Down Expand Up @@ -705,6 +709,12 @@ def parse_interaction_create(self, data) -> None:
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)
if interaction.type == InteractionType.modal_submit:
user_id, custom_id = (
interaction.user.id,
interaction.data["custom_id"],
)
asyncio.create_task(self._modal_store.dispatch(user_id, custom_id, interaction))

self.dispatch('interaction', interaction)

Expand Down
20 changes: 18 additions & 2 deletions discord/types/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
from typing import List, Literal, TypedDict, Union
from .emoji import PartialEmoji

ComponentType = Literal[1, 2, 3]
ComponentType = Literal[1, 2, 3, 4]
ButtonStyle = Literal[1, 2, 3, 4, 5]
InputTextStyle = Literal[1, 2]


class ActionRow(TypedDict):
Expand All @@ -50,6 +51,21 @@ class ButtonComponent(_ButtonComponentOptional):
style: ButtonStyle


class _InputTextComponentOptional(TypedDict, total=False):
min_length: int
max_length: int
required: bool
placeholder: str
value: str


class InputText(_InputTextComponentOptional):
type: Literal[4]
style: InputTextStyle
custom_id: str
label: str


class _SelectMenuOptional(TypedDict, total=False):
placeholder: str
min_values: int
Expand All @@ -74,4 +90,4 @@ class SelectMenu(_SelectMenuOptional):
options: List[SelectOption]


Component = Union[ActionRow, ButtonComponent, SelectMenu]
Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText]
2 changes: 2 additions & 0 deletions discord/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
from .item import *
from .button import *
from .select import *
from .input_text import *
from .modal import *
Loading

0 comments on commit 8690471

Please sign in to comment.