Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions interactions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
Invite,
InviteTargetType,
is_owner,
LabelComponent,
LeakyBucketSystem,
listen,
Listener,
Expand Down Expand Up @@ -562,6 +563,7 @@
"InviteTargetType",
"is_owner",
"kwarg_spam",
"LabelComponent",
"LeakyBucketSystem",
"listen",
"Listener",
Expand Down
2 changes: 2 additions & 0 deletions interactions/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
InvitableMixin,
Invite,
InviteTargetType,
LabelComponent,
MaterialColors,
MaterialColours,
MediaGalleryComponent,
Expand Down Expand Up @@ -492,6 +493,7 @@
"Invite",
"InviteTargetType",
"is_owner",
"LabelComponent",
"LeakyBucketSystem",
"listen",
"Listener",
Expand Down
3 changes: 2 additions & 1 deletion interactions/models/discord/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
process_message_payload,
process_message_reference,
)
from .modal import InputText, Modal, ParagraphText, ShortText, TextStyles
from .modal import InputText, Modal, ParagraphText, ShortText, TextStyles, LabelComponent
from .onboarding import Onboarding, OnboardingPrompt, OnboardingPromptOption
from .poll import PollMedia, PollAnswer, PollAnswerCount, PollResults, Poll
from .reaction import Reaction, ReactionUsers
Expand Down Expand Up @@ -287,6 +287,7 @@
"InvitableMixin",
"Invite",
"InviteTargetType",
"LabelComponent",
"MaterialColors",
"MaterialColours",
"MediaGalleryComponent",
Expand Down
30 changes: 26 additions & 4 deletions interactions/models/discord/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,8 @@ class BaseSelectMenu(InteractiveComponent):
min_values Optional[int]: The minimum number of items that must be chosen. (default 1, min 0, max 25)
max_values Optional[int]: The maximum number of items that can be chosen. (default 1, max 25)
disabled bool: Disable the select and make it not intractable, default false.
type Union[ComponentType, int]: The action role type number defined by discord. This cannot be modified.
type Union[ComponentType, int]: The type of component, as defined by discord. This cannot be modified.
required bool: Whether this select menu is required to be filled out or not in modals.

"""

Expand All @@ -377,12 +378,14 @@ def __init__(
max_values: int = 1,
custom_id: str | None = None,
disabled: bool = False,
required: bool = True,
) -> None:
self.custom_id: str = custom_id or str(uuid.uuid4())
self.placeholder: str | None = placeholder
self.min_values: int = min_values
self.max_values: int = max_values
self.disabled: bool = disabled
self.required: bool = required

self.type: ComponentType = MISSING

Expand All @@ -394,10 +397,11 @@ def from_dict(cls, data: discord_typings.SelectMenuComponentData) -> "BaseSelect
max_values=data["max_values"],
custom_id=data["custom_id"],
disabled=data.get("disabled", False),
required=data.get("required", True),
)

def __repr__(self) -> str:
return f"<{self.__class__.__name__} type={self.type} custom_id={self.custom_id} placeholder={self.placeholder} min_values={self.min_values} max_values={self.max_values} disabled={self.disabled}>"
return f"<{self.__class__.__name__} type={self.type} custom_id={self.custom_id} placeholder={self.placeholder} min_values={self.min_values} max_values={self.max_values} disabled={self.disabled} required={self.required}>"

def to_dict(self) -> discord_typings.SelectMenuComponentData:
return {
Expand All @@ -407,6 +411,7 @@ def to_dict(self) -> discord_typings.SelectMenuComponentData:
"min_values": self.min_values,
"max_values": self.max_values,
"disabled": self.disabled,
"required": self.required,
}


Expand Down Expand Up @@ -586,6 +591,7 @@ class StringSelectMenu(BaseSelectMenu):
max_values Optional[int]: The maximum number of items that can be chosen. (default 1, max 25)
disabled bool: Disable the select and make it not intractable, default false.
type Union[ComponentType, int]: The type of component, as defined by discord. This cannot be modified.
required bool: Whether this select menu is required to be filled out or not in modals.

"""

Expand All @@ -600,13 +606,15 @@ def __init__(
max_values: int = 1,
custom_id: str | None = None,
disabled: bool = False,
required: bool = True,
) -> None:
super().__init__(
placeholder=placeholder,
min_values=min_values,
max_values=max_values,
custom_id=custom_id,
disabled=disabled,
required=required,
)
if isinstance(options, (list, tuple)) and len(options) == 1 and isinstance(options[0], (list, tuple)):
# user passed in a list of options, expand it out
Expand All @@ -624,10 +632,11 @@ def from_dict(cls, data: discord_typings.SelectMenuComponentData) -> "StringSele
max_values=data["max_values"],
custom_id=data["custom_id"],
disabled=data.get("disabled", False),
required=data.get("required", True),
)

def __repr__(self) -> str:
return f"<{self.__class__.__name__} type={self.type} custom_id={self.custom_id} placeholder={self.placeholder} min_values={self.min_values} max_values={self.max_values} disabled={self.disabled} options={self.options}>"
return f"<{self.__class__.__name__} type={self.type} custom_id={self.custom_id} placeholder={self.placeholder} min_values={self.min_values} max_values={self.max_values} disabled={self.disabled} required={self.required} options={self.options}>"

def to_dict(self) -> discord_typings.SelectMenuComponentData:
return {
Expand All @@ -648,6 +657,7 @@ class UserSelectMenu(DefaultableSelectMenu):
default_values list[BaseUser, Member, SelectDefaultValues]: A list of default values to pre-select in the select.
disabled bool: Disable the select and make it not intractable, default false.
type Union[ComponentType, int]: The type of component, as defined by discord. This cannot be modified.
required bool: Whether this select menu is required to be filled out or not in modals.

"""

Expand All @@ -669,13 +679,15 @@ def __init__(
| None
) = None,
disabled: bool = False,
required: bool = True,
) -> None:
super().__init__(
placeholder=placeholder,
min_values=min_values,
max_values=max_values,
custom_id=custom_id,
disabled=disabled,
required=required,
defaults=default_values,
)

Expand All @@ -694,6 +706,7 @@ class RoleSelectMenu(DefaultableSelectMenu):
default_values list[Role, SelectDefaultValues]: A list of default values to pre-select in the select.
disabled bool: Disable the select and make it not intractable, default false.
type Union[ComponentType, int]: The type of component, as defined by discord. This cannot be modified.
required bool: Whether this select menu is required to be filled out or not in modals.

"""

Expand All @@ -705,6 +718,7 @@ def __init__(
max_values: int = 1,
custom_id: str | None = None,
disabled: bool = False,
required: bool = True,
default_values: (
list[
Union[
Expand All @@ -721,6 +735,7 @@ def __init__(
max_values=max_values,
custom_id=custom_id,
disabled=disabled,
required=required,
defaults=default_values,
)

Expand All @@ -739,6 +754,7 @@ class MentionableSelectMenu(DefaultableSelectMenu):
default_values list[BaseUser, Role, BaseChannel, Member, SelectDefaultValues]: A list of default values to pre-select in the select.
disabled bool: Disable the select and make it not intractable, default false.
type Union[ComponentType, int]: The type of component, as defined by discord. This cannot be modified.
required bool: Whether this select menu is required to be filled out or not in modals.

"""

Expand All @@ -750,6 +766,7 @@ def __init__(
max_values: int = 1,
custom_id: str | None = None,
disabled: bool = False,
required: bool = True,
default_values: (
list[
Union[
Expand All @@ -769,6 +786,7 @@ def __init__(
max_values=max_values,
custom_id=custom_id,
disabled=disabled,
required=required,
defaults=default_values,
)

Expand All @@ -787,6 +805,7 @@ class ChannelSelectMenu(DefaultableSelectMenu):
default_values list[BaseChannel, SelectDefaultValues]: A list of default values to pre-select in the select.
disabled bool: Disable the select and make it not intractable, default false.
type Union[ComponentType, int]: The type of component, as defined by discord. This cannot be modified.
required bool: Whether this select menu is required to be filled out or not in modals.

"""

Expand All @@ -799,6 +818,7 @@ def __init__(
max_values: int = 1,
custom_id: str | None = None,
disabled: bool = False,
required: bool = True,
default_values: (
list[
Union[
Expand All @@ -815,6 +835,7 @@ def __init__(
max_values=max_values,
custom_id=custom_id,
disabled=disabled,
required=required,
defaults=default_values,
)

Expand All @@ -831,11 +852,12 @@ def from_dict(cls, data: discord_typings.SelectMenuComponentData) -> "ChannelSel
max_values=data["max_values"],
custom_id=data["custom_id"],
disabled=data.get("disabled", False),
required=data.get("required", True),
channel_types=data.get("channel_types", []),
)

def __repr__(self) -> str:
return f"<{self.__class__.__name__} type={self.type} custom_id={self.custom_id} placeholder={self.placeholder} min_values={self.min_values} max_values={self.max_values} disabled={self.disabled} channel_types={self.channel_types}>"
return f"<{self.__class__.__name__} type={self.type} custom_id={self.custom_id} placeholder={self.placeholder} min_values={self.min_values} max_values={self.max_values} disabled={self.disabled} required={self.required} channel_types={self.channel_types}>"

def to_dict(self) -> discord_typings.SelectMenuComponentData:
return {
Expand Down
2 changes: 2 additions & 0 deletions interactions/models/discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,8 @@ class ComponentType(CursedIntEnum):
"""Separator component for visual separation"""
CONTAINER = 17
"""Container component for grouping together other components"""
LABEL = 18
"""Label component for modals"""

# TODO: this is hacky, is there a better way to do this?
@staticmethod
Expand Down
101 changes: 87 additions & 14 deletions interactions/models/discord/modal.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import uuid
from enum import IntEnum
from typing import Union, Optional, Any, TypeVar
from typing_extensions import Self

import discord_typings

from interactions.client.const import MISSING
from interactions.client.mixins.serialization import DictSerializationMixin
from interactions.client.utils import dict_filter
from interactions.models.discord.components import ComponentType
from interactions.client.utils import dict_filter, dict_filter_none
from interactions.models.discord.components import (
ChannelSelectMenu,
ComponentType,
BaseComponent,
BaseSelectMenu,
MentionableSelectMenu,
RoleSelectMenu,
StringSelectMenu,
UserSelectMenu,
)
from interactions.models.internal.application_commands import CallbackType

__all__ = ("InputText", "Modal", "ParagraphText", "ShortText", "TextStyles")
__all__ = ("InputText", "Modal", "ParagraphText", "ShortText", "TextStyles", "LabelComponent")

T = TypeVar("T", bound="InputText")

Expand Down Expand Up @@ -62,7 +72,7 @@ def to_dict(
)

@classmethod
def from_dict(cls, data: dict[str, Any]) -> T:
def from_dict(cls, data: dict[str, Any]) -> Self:
if data["style"] == TextStyles.SHORT:
cls = ShortText
elif data["style"] == TextStyles.PARAGRAPH:
Expand Down Expand Up @@ -127,36 +137,99 @@ def __init__(
)


class LabelComponent(BaseComponent):
"""
A top-level layout component that wraps modal components with text as a label and optional description.

Attributes:
label: The text label for the component.
description: An optional description for the component.
component: The component to be wrapped, either an InputText or a select menu.
type: The type of the component, always ComponentType.LABEL.

"""

def __init__(
self,
*,
label: str,
description: Optional[str] = None,
component: BaseSelectMenu | InputText,
):
self.label = label
self.component = component
self.description = description
self.type = ComponentType.LABEL

def to_dict(self) -> dict:
return dict_filter_none(
{
"type": self.type,
"label": self.label,
"description": self.description,
"component": self.component.to_dict() if hasattr(self.component, "to_dict") else self.component,
}
)

@classmethod
def from_dict(cls, data: dict) -> Self:
return cls(
label=data["label"],
description=data.get("description"),
component=BaseComponent.from_dict_factory(
data["component"],
alternate_mapping={
ComponentType.INPUT_TEXT: InputText,
ComponentType.CHANNEL_SELECT: ChannelSelectMenu,
ComponentType.STRING_SELECT: StringSelectMenu,
ComponentType.USER_SELECT: UserSelectMenu,
ComponentType.ROLE_SELECT: RoleSelectMenu,
ComponentType.MENTIONABLE_SELECT: MentionableSelectMenu,
},
),
)


class Modal:
def __init__(
self,
*components: InputText,
*components: InputText | LabelComponent,
title: str,
custom_id: Optional[str] = None,
) -> None:
self.title: str = title
self.components: list[InputText] = list(components)
self.components: list[InputText | LabelComponent] = list(components)
self.custom_id: str = custom_id or str(uuid.uuid4())

self.type = CallbackType.MODAL

def to_dict(self) -> discord_typings.ModalInteractionData:
dict_components: list[dict] = []

for component in self.components:
if isinstance(component, InputText):
dict_components.append({"type": ComponentType.ACTION_ROW, "components": [component.to_dict()]})
elif isinstance(component, LabelComponent):
dict_components.append(component.to_dict())
else:
# backwards compatibility behavior, remove in v6
dict_components.append(
{
"type": ComponentType.ACTION_ROW,
"components": [component],
}
)

return {
"type": self.type,
"data": {
"title": self.title,
"custom_id": self.custom_id,
"components": [
{
"type": ComponentType.ACTION_ROW,
"components": [c.to_dict() if hasattr(c, "to_dict") else c],
}
for c in self.components
],
"components": dict_components,
},
}

def add_components(self, *components: InputText) -> None:
def add_components(self, *components: InputText | LabelComponent) -> None:
"""
Add components to the modal.

Expand Down
Loading
Loading