Skip to content

Commit

Permalink
feat: select menu default values (#1115)
Browse files Browse the repository at this point in the history
  • Loading branch information
shiftinv authored Nov 14, 2024
1 parent 0961c9d commit ef7d82f
Show file tree
Hide file tree
Showing 20 changed files with 532 additions and 76 deletions.
1 change: 1 addition & 0 deletions changelog/1115.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :class:`SelectDefaultValue`, and add :attr:`~UserSelectMenu.default_values` to all auto-populated select menu types.
8 changes: 3 additions & 5 deletions disnake/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from .permissions import PermissionOverwrite, Permissions
from .role import Role
from .sticker import GuildSticker, StandardSticker, StickerItem
from .ui.action_row import components_to_dict
from .utils import _overload_with_permissions
from .voice_client import VoiceClient, VoiceProtocol

Expand Down Expand Up @@ -179,6 +178,7 @@ def avatar(self) -> Optional[Asset]:
raise NotImplementedError


# FIXME: this shouldn't be a protocol. isinstance(thread, PrivateChannel) returns true, and issubclass doesn't work.
@runtime_checkable
class PrivateChannel(Snowflake, Protocol):
"""An ABC that details the common operations on a private Discord channel.
Expand Down Expand Up @@ -1719,16 +1719,14 @@ async def send(

if view is not None and components is not None:
raise TypeError("cannot pass both view and components parameter to send()")

elif view:
if not hasattr(view, "__discord_ui_view__"):
raise TypeError(f"view parameter must be View not {view.__class__!r}")

components_payload = view.to_components()

elif components:
components_payload = components_to_dict(components)
from .ui.action_row import components_to_dict

components_payload = components_to_dict(components)
else:
components_payload = None

Expand Down
1 change: 1 addition & 0 deletions disnake/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5092,6 +5092,7 @@ def _channel_type_factory(
cls: Union[Type[disnake.abc.GuildChannel], Type[Thread]]
) -> List[ChannelType]:
return {
# FIXME: this includes private channels; improve this once there's a common base type for all channels
disnake.abc.GuildChannel: list(ChannelType.__members__.values()),
VocalGuildChannel: [ChannelType.voice, ChannelType.stage_voice],
disnake.abc.PrivateChannel: [ChannelType.private, ChannelType.group],
Expand Down
83 changes: 81 additions & 2 deletions disnake/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@
cast,
)

from .enums import ButtonStyle, ChannelType, ComponentType, TextInputStyle, try_enum
from .enums import (
ButtonStyle,
ChannelType,
ComponentType,
SelectDefaultValueType,
TextInputStyle,
try_enum,
)
from .partial_emoji import PartialEmoji, _EmojiTag
from .utils import MISSING, assert_never, get_slots

Expand All @@ -35,6 +42,7 @@
Component as ComponentPayload,
MentionableSelectMenu as MentionableSelectMenuPayload,
RoleSelectMenu as RoleSelectMenuPayload,
SelectDefaultValue as SelectDefaultValuePayload,
SelectOption as SelectOptionPayload,
StringSelectMenu as StringSelectMenuPayload,
TextInput as TextInputPayload,
Expand All @@ -53,6 +61,7 @@
"MentionableSelectMenu",
"ChannelSelectMenu",
"SelectOption",
"SelectDefaultValue",
"TextInput",
)

Expand Down Expand Up @@ -264,6 +273,12 @@ class BaseSelectMenu(Component):
A list of options that can be selected in this select menu.
disabled: :class:`bool`
Whether the select menu is disabled or not.
default_values: List[:class:`SelectDefaultValue`]
The list of values (users/roles/channels) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
Only available for auto-populated select menus.
.. versionadded:: 2.10
"""

__slots__: Tuple[str, ...] = (
Expand All @@ -272,9 +287,11 @@ class BaseSelectMenu(Component):
"min_values",
"max_values",
"disabled",
"default_values",
)

__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
# FIXME: this isn't pretty; we should decouple __repr__ from slots
__repr_info__: ClassVar[Tuple[str, ...]] = tuple(s for s in __slots__ if s != "default_values")

# n.b: ideally this would be `BaseSelectMenuPayload`,
# but pyright made TypedDict keys invariant and doesn't
Expand All @@ -288,6 +305,9 @@ def __init__(self, data: AnySelectMenuPayload) -> None:
self.min_values: int = data.get("min_values", 1)
self.max_values: int = data.get("max_values", 1)
self.disabled: bool = data.get("disabled", False)
self.default_values: List[SelectDefaultValue] = [
SelectDefaultValue._from_dict(d) for d in (data.get("default_values") or [])
]

def to_dict(self) -> BaseSelectMenuPayload:
payload: BaseSelectMenuPayload = {
Expand All @@ -301,6 +321,9 @@ def to_dict(self) -> BaseSelectMenuPayload:
if self.placeholder:
payload["placeholder"] = self.placeholder

if self.default_values:
payload["default_values"] = [v.to_dict() for v in self.default_values]

return payload


Expand Down Expand Up @@ -377,6 +400,11 @@ class UserSelectMenu(BaseSelectMenu):
Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool`
Whether the select menu is disabled or not.
default_values: List[:class:`SelectDefaultValue`]
The list of values (users/members) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
.. versionadded:: 2.10
"""

__slots__: Tuple[str, ...] = ()
Expand Down Expand Up @@ -412,6 +440,11 @@ class RoleSelectMenu(BaseSelectMenu):
Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool`
Whether the select menu is disabled or not.
default_values: List[:class:`SelectDefaultValue`]
The list of values (roles) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
.. versionadded:: 2.10
"""

__slots__: Tuple[str, ...] = ()
Expand Down Expand Up @@ -447,6 +480,11 @@ class MentionableSelectMenu(BaseSelectMenu):
Defaults to 1 and must be between 1 and 25.
disabled: :class:`bool`
Whether the select menu is disabled or not.
default_values: List[:class:`SelectDefaultValue`]
The list of values (users/roles) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
.. versionadded:: 2.10
"""

__slots__: Tuple[str, ...] = ()
Expand Down Expand Up @@ -485,6 +523,11 @@ class ChannelSelectMenu(BaseSelectMenu):
channel_types: Optional[List[:class:`ChannelType`]]
A list of channel types that can be selected in this select menu.
If ``None``, channels of all types may be selected.
default_values: List[:class:`SelectDefaultValue`]
The list of values (channels) that are selected by default.
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
.. versionadded:: 2.10
"""

__slots__: Tuple[str, ...] = ("channel_types",)
Expand Down Expand Up @@ -613,6 +656,42 @@ def to_dict(self) -> SelectOptionPayload:
return payload


class SelectDefaultValue:
"""Represents a default value of an auto-populated select menu (currently all
select menu types except :class:`StringSelectMenu`).
Depending on the :attr:`type` attribute, this can represent different types of objects.
.. versionadded:: 2.10
Attributes
----------
id: :class:`int`
The ID of the target object.
type: :class:`SelectDefaultValueType`
The type of the target object.
"""

__slots__: Tuple[str, ...] = ("id", "type")

def __init__(self, id: int, type: SelectDefaultValueType) -> None:
self.id: int = id
self.type: SelectDefaultValueType = type

@classmethod
def _from_dict(cls, data: SelectDefaultValuePayload) -> Self:
return cls(int(data["id"]), try_enum(SelectDefaultValueType, data["type"]))

def to_dict(self) -> SelectDefaultValuePayload:
return {
"id": self.id,
"type": self.type.value,
}

def __repr__(self) -> str:
return f"<SelectDefaultValue id={self.id!r} type={self.type.value!r}>"


class TextInput(Component):
"""Represents a text input from the Discord Bot UI Kit.
Expand Down
10 changes: 10 additions & 0 deletions disnake/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"ComponentType",
"ButtonStyle",
"TextInputStyle",
"SelectDefaultValueType",
"StagePrivacyLevel",
"InteractionType",
"InteractionResponseType",
Expand Down Expand Up @@ -694,6 +695,15 @@ def __int__(self) -> int:
return self.value


class SelectDefaultValueType(Enum):
user = "user"
role = "role"
channel = "channel"

def __str__(self) -> str:
return self.value


class ApplicationCommandType(Enum):
chat_input = 1
user = 2
Expand Down
9 changes: 9 additions & 0 deletions disnake/types/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

from .channel import ChannelType
from .emoji import PartialEmoji
from .snowflake import Snowflake

ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8]
ButtonStyle = Literal[1, 2, 3, 4, 5]
TextInputStyle = Literal[1, 2]

SelectDefaultValueType = Literal["user", "role", "channel"]

Component = Union["ActionRow", "ButtonComponent", "AnySelectMenu", "TextInput"]

Expand Down Expand Up @@ -40,12 +42,19 @@ class SelectOption(TypedDict):
default: NotRequired[bool]


class SelectDefaultValue(TypedDict):
id: Snowflake
type: SelectDefaultValueType


class _SelectMenu(TypedDict):
custom_id: str
placeholder: NotRequired[str]
min_values: NotRequired[int]
max_values: NotRequired[int]
disabled: NotRequired[bool]
# This is technically not applicable to string selects, but for simplicity we'll just have it here
default_values: NotRequired[List[SelectDefaultValue]]


class BaseSelectMenu(_SelectMenu):
Expand Down
6 changes: 4 additions & 2 deletions disnake/types/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ class InteractionDataResolved(TypedDict, total=False):
members: Dict[Snowflake, Member]
roles: Dict[Snowflake, Role]
channels: Dict[Snowflake, InteractionChannel]
# only in application commands


class ApplicationCommandInteractionDataResolved(InteractionDataResolved, total=False):
messages: Dict[Snowflake, Message]
attachments: Dict[Snowflake, Attachment]

Expand Down Expand Up @@ -158,7 +160,7 @@ class ApplicationCommandInteractionData(TypedDict):
id: Snowflake
name: str
type: ApplicationCommandType
resolved: NotRequired[InteractionDataResolved]
resolved: NotRequired[ApplicationCommandInteractionDataResolved]
options: NotRequired[List[ApplicationCommandInteractionDataOption]]
# this is the guild the command is registered to, not the guild the command was invoked in (see interaction.guild_id)
guild_id: NotRequired[Snowflake]
Expand Down
4 changes: 3 additions & 1 deletion disnake/types/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .components import Component
from .embed import Embed
from .emoji import PartialEmoji
from .interactions import InteractionMessageReference
from .interactions import InteractionDataResolved, InteractionMessageReference
from .member import Member, UserWithMember
from .poll import Poll
from .snowflake import Snowflake, SnowflakeList
Expand Down Expand Up @@ -117,6 +117,8 @@ class Message(TypedDict):
position: NotRequired[int]
role_subscription_data: NotRequired[RoleSubscriptionData]
poll: NotRequired[Poll]
# contains resolved objects for `default_values` of select menus in this message; we currently don't have a use for this
resolved: NotRequired[InteractionDataResolved]

# specific to MESSAGE_CREATE/MESSAGE_UPDATE events
guild_id: NotRequired[Snowflake]
Expand Down
Loading

0 comments on commit ef7d82f

Please sign in to comment.