Skip to content

Commit

Permalink
feat(components): initial modal handler implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
tandemdude committed Aug 11, 2024
1 parent bac7c72 commit d027521
Show file tree
Hide file tree
Showing 8 changed files with 497 additions and 115 deletions.
11 changes: 11 additions & 0 deletions lightbulb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Client(abc.ABC):
"_extensions",
"_localization",
"_menu_queues",
"_modal_queues",
"_owner_ids",
"_registered_commands",
"_started",
Expand Down Expand Up @@ -173,6 +174,7 @@ def __init__(

self._tasks: set[tasks.Task] = set()
self._menu_queues: set[asyncio.Queue[hikari.ComponentInteraction]] = set()
self._modal_queues: set[asyncio.Queue[hikari.ModalInteraction]] = set()

self._asyncio_tasks: set[asyncio.Task[t.Any]] = set()

Expand Down Expand Up @@ -1011,13 +1013,22 @@ async def handle_component_interaction(self, interaction: hikari.ComponentIntera

await asyncio.gather(*(q.put(interaction) for q in self._menu_queues))

async def handle_modal_interaction(self, interaction: hikari.ModalInteraction) -> None:
if not self._started:
LOGGER.debug("ignoring modal interaction received before the client was started")
return

await asyncio.gather(*(q.put(interaction) for q in self._modal_queues))

async def handle_interaction_create(self, interaction: hikari.PartialInteraction) -> None:
if isinstance(interaction, hikari.AutocompleteInteraction):
await self.handle_autocomplete_interaction(interaction)
elif isinstance(interaction, hikari.CommandInteraction):
await self.handle_application_command_interaction(interaction)
elif isinstance(interaction, hikari.ComponentInteraction):
await self.handle_component_interaction(interaction)
elif isinstance(interaction, hikari.ModalInteraction):
await self.handle_modal_interaction(interaction)


class GatewayEnabledClient(Client):
Expand Down
3 changes: 3 additions & 0 deletions lightbulb/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@
"MentionableSelect",
"Menu",
"MenuContext",
"Modal",
"ModalContext",
"RoleSelect",
"Select",
"TextInput",
"TextSelect",
"UserSelect",
]
153 changes: 153 additions & 0 deletions lightbulb/components/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@
import abc
import typing as t

import hikari
from hikari.api import special_endpoints

from lightbulb import context
from lightbulb.internal import constants

if t.TYPE_CHECKING:
from collections.abc import Sequence

RowT = t.TypeVar("RowT", special_endpoints.MessageActionRowBuilder, special_endpoints.ModalActionRowBuilder)


Expand All @@ -39,3 +46,149 @@ def custom_id(self) -> str: ...

@abc.abstractmethod
def add_to_row(self, row: RowT) -> RowT: ...


class MessageResponseMixinWithEdit(context.MessageResponseMixin[context.RespondableInteractionT], abc.ABC):
__slots__ = ()

async def defer(self, *, ephemeral: bool = False, edit: bool = False) -> None:
"""
Defer the creation of a response for the interaction that this context represents.
Args:
ephemeral: Whether to defer ephemerally (message only visible to the user that triggered
the command).
edit: Whether the eventual response should cause an edit instead of creating a new message.
Returns:
:obj:`None`
"""
async with self._response_lock:
if self._initial_response_sent:
return

response_type = (
hikari.ResponseType.DEFERRED_MESSAGE_UPDATE if edit else hikari.ResponseType.DEFERRED_MESSAGE_CREATE
)
await self._create_initial_response(
response_type,
flags=hikari.MessageFlag.EPHEMERAL if ephemeral else hikari.MessageFlag.NONE,
)
self._initial_response_sent = True

async def respond(
self,
content: hikari.UndefinedOr[t.Any] = hikari.UNDEFINED,
*,
ephemeral: bool = False,
edit: bool = False,
flags: int | hikari.MessageFlag | hikari.UndefinedType = hikari.UNDEFINED,
tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
attachments: hikari.UndefinedOr[Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
component: hikari.UndefinedOr[special_endpoints.ComponentBuilder] = hikari.UNDEFINED,
components: hikari.UndefinedOr[Sequence[special_endpoints.ComponentBuilder]] = hikari.UNDEFINED,
embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
embeds: hikari.UndefinedOr[Sequence[hikari.Embed]] = hikari.UNDEFINED,
mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
user_mentions: hikari.UndefinedOr[hikari.SnowflakeishSequence[hikari.PartialUser] | bool] = hikari.UNDEFINED,
role_mentions: hikari.UndefinedOr[hikari.SnowflakeishSequence[hikari.PartialRole] | bool] = hikari.UNDEFINED,
) -> hikari.Snowflakeish:
"""
Create a response to the interaction that this context represents.
Args:
content: The message contents.
ephemeral: Whether the message should be ephemeral (only visible to the user that triggered the command).
This is just a convenience argument - passing `flags=hikari.MessageFlag.EPHEMERAL` will function
the same way.
edit: Whether the response should cause an edit instead of creating a new message.
attachment: The message attachment.
attachments: The message attachments.
component: The builder object of the component to include in this message.
components: The sequence of the component builder objects to include in this message.
embed: The message embed.
embeds: The message embeds.
flags: The message flags this response should have.
tts: Whether the message will be read out by a screen reader using Discord's TTS (text-to-speech) system.
mentions_everyone: Whether the message should parse @everyone/@here mentions.
user_mentions: The user mentions to include in the message.
role_mentions: The role mentions to include in the message.
Returns:
:obj:`hikari.snowflakes.Snowflakeish`: An identifier for the response. This can then be used to edit,
delete, or fetch the response message using the appropriate methods.
Note:
This documentation does not contain a full description of the parameters as they would just
be copy-pasted from the hikari documentation. See
:obj:`~hikari.interactions.base_interactions.MessageResponseMixin.create_initial_response` for a more
detailed description.
Note:
If this is **not** creating an initial response and ``edit`` is :obj:True`, then this will **always** edit
the initial response, not the most recently created response.
See Also:
:meth:`~MenuContext.edit_response`
:meth:`~MenuContext.delete_response`
:meth:`~MenuContext.fetch_response`
"""
if ephemeral:
flags = (flags or hikari.MessageFlag.NONE) | hikari.MessageFlag.EPHEMERAL

async with self._response_lock:
if not self._initial_response_sent:
await self._create_initial_response(
hikari.ResponseType.MESSAGE_UPDATE if edit else hikari.ResponseType.MESSAGE_CREATE,
content,
flags=flags,
tts=tts,
attachment=attachment,
attachments=attachments,
component=component,
components=components,
embed=embed,
embeds=embeds,
mentions_everyone=mentions_everyone,
user_mentions=user_mentions,
role_mentions=role_mentions,
)
self._initial_response_sent = True
return constants.INITIAL_RESPONSE_IDENTIFIER
else:
if edit:
return (
await self.edit_response(
constants.INITIAL_RESPONSE_IDENTIFIER,
content,
attachment=attachment,
attachments=attachments,
component=component,
components=components,
embed=embed,
embeds=embeds,
mentions_everyone=mentions_everyone,
user_mentions=user_mentions,
role_mentions=role_mentions,
)
).id
# This will automatically cause a response if the initial response was deferred previously.
# I am not sure if this is intentional by discord however so, we may want to look into changing
# this to actually edit the initial response if it was previously deferred.
return (
await self.interaction.execute(
content,
flags=flags,
tts=tts,
attachment=attachment,
attachments=attachments,
component=component,
components=components,
embed=embed,
embeds=embeds,
mentions_everyone=mentions_everyone,
user_mentions=user_mentions,
role_mentions=role_mentions,
)
).id
113 changes: 38 additions & 75 deletions lightbulb/components/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
from hikari.api import special_endpoints
from hikari.impl import special_endpoints as special_endpoints_impl

from lightbulb import context
from lightbulb.components import base

if t.TYPE_CHECKING:
Expand All @@ -65,8 +64,6 @@
T = t.TypeVar("T")
MessageComponentT = t.TypeVar("MessageComponentT", bound=base.BaseComponent[special_endpoints.MessageActionRowBuilder])

INITIAL_RESPONSE_IDENTIFIER: t.Final[int] = -1


@dataclasses.dataclass(slots=True, kw_only=True)
class InteractiveButton(base.BaseComponent[special_endpoints.MessageActionRowBuilder]):
Expand Down Expand Up @@ -210,7 +207,7 @@ def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_


@dataclasses.dataclass(slots=True, kw_only=True)
class MenuContext(context.MessageResponseMixin[hikari.ComponentInteraction]):
class MenuContext(base.MessageResponseMixinWithEdit[hikari.ComponentInteraction]):
menu: Menu
interaction: hikari.ComponentInteraction
component: base.BaseComponent[special_endpoints.MessageActionRowBuilder]
Expand Down Expand Up @@ -266,29 +263,33 @@ def selected_values_for(self, select: Select[T]) -> Sequence[T]:

return resolved

async def defer(self, *, ephemeral: bool = False, edit: bool = False) -> None:
async def respond_with_modal(
self,
title: str,
custom_id: str,
component: hikari.UndefinedOr[special_endpoints.ComponentBuilder] = hikari.UNDEFINED,
components: hikari.UndefinedOr[Sequence[special_endpoints.ComponentBuilder]] = hikari.UNDEFINED,
) -> None:
"""
Defer the creation of a response for the interaction that this context represents.
Create a modal response to the interaction that this context represents.
Args:
ephemeral: Whether to defer ephemerally (message only visible to the user that triggered
the command).
edit: Whether the eventual response should cause an edit instead of creating a new message.
title: The title that will show up in the modal.
custom_id: Developer set custom ID used for identifying interactions with this modal.
component: A component builder to send in this modal.
components: A sequence of component builders to send in this modal.
Returns:
:obj:`None`
Raises:
:obj:`RuntimeError`: If an initial response has already been sent.
"""
async with self._response_lock:
if self._initial_response_sent:
return
raise RuntimeError("cannot respond with a modal if an initial response has already been sent")

response_type = (
hikari.ResponseType.DEFERRED_MESSAGE_UPDATE if edit else hikari.ResponseType.DEFERRED_MESSAGE_CREATE
)
await self._create_initial_response(
response_type,
flags=hikari.MessageFlag.EPHEMERAL if ephemeral else hikari.MessageFlag.NONE,
)
await self.interaction.create_modal_response(title, custom_id, component, components)
self._initial_response_sent = True

async def respond(
Expand Down Expand Up @@ -353,67 +354,25 @@ async def respond(
:meth:`~MenuContext.delete_response`
:meth:`~MenuContext.fetch_response`
"""
if ephemeral:
flags = (flags or hikari.MessageFlag.NONE) | hikari.MessageFlag.EPHEMERAL

if rebuild_menu:
components = components if components is not hikari.UNDEFINED else self.menu

async with self._response_lock:
if not self._initial_response_sent:
await self._create_initial_response(
hikari.ResponseType.MESSAGE_UPDATE if edit else hikari.ResponseType.MESSAGE_CREATE,
content,
flags=flags,
tts=tts,
attachment=attachment,
attachments=attachments,
component=component,
components=components,
embed=embed,
embeds=embeds,
mentions_everyone=mentions_everyone,
user_mentions=user_mentions,
role_mentions=role_mentions,
)
self._initial_response_sent = True
return INITIAL_RESPONSE_IDENTIFIER
else:
if edit:
return (
await self.edit_response(
INITIAL_RESPONSE_IDENTIFIER,
content,
attachment=attachment,
attachments=attachments,
component=component,
components=components,
embed=embed,
embeds=embeds,
mentions_everyone=mentions_everyone,
user_mentions=user_mentions,
role_mentions=role_mentions,
)
).id
# This will automatically cause a response if the initial response was deferred previously.
# I am not sure if this is intentional by discord however so, we may want to look into changing
# this to actually edit the initial response if it was previously deferred.
return (
await self.interaction.execute(
content,
flags=flags,
tts=tts,
attachment=attachment,
attachments=attachments,
component=component,
components=components,
embed=embed,
embeds=embeds,
mentions_everyone=mentions_everyone,
user_mentions=user_mentions,
role_mentions=role_mentions,
)
).id
return await super().respond(
content,
ephemeral=ephemeral,
edit=edit,
flags=flags,
tts=tts,
attachment=attachment,
attachments=attachments,
component=component,
components=components,
embed=embed,
embeds=embeds,
mentions_everyone=mentions_everyone,
user_mentions=user_mentions,
role_mentions=role_mentions,
)


class Menu(Sequence[special_endpoints.ComponentBuilder]):
Expand Down Expand Up @@ -477,6 +436,10 @@ def clear_rows(self) -> t_ex.Self:
self._rows.clear()
return self

def clear_current_row(self) -> t_ex.Self:
self._rows[self._current_row].clear()
return self

def next_row(self) -> t_ex.Self:
if self._current_row + 1 >= self._MAX_ROWS:
raise RuntimeError("the maximum number of rows has been reached")
Expand Down
Loading

0 comments on commit d027521

Please sign in to comment.