Skip to content

Commit

Permalink
feat(components): initial implementation of a component menu and handler
Browse files Browse the repository at this point in the history
  • Loading branch information
tandemdude committed Aug 10, 2024
1 parent ee0b07f commit 6a6b58a
Show file tree
Hide file tree
Showing 11 changed files with 928 additions and 88 deletions.
1 change: 1 addition & 0 deletions fragments/+defer.removal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The `ephemeral` argument for `Context.defer()` is now keyword-only.
2 changes: 2 additions & 0 deletions lightbulb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
# SOFTWARE.
"""A simple-to-use command handler for Hikari."""

from lightbulb import components
from lightbulb import di
from lightbulb import exceptions
from lightbulb import internal
Expand Down Expand Up @@ -62,6 +63,7 @@
"boolean",
"channel",
"client_from_app",
"components",
"crontrigger",
"di",
"exceptions",
Expand Down
33 changes: 30 additions & 3 deletions lightbulb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,14 @@ class Client(abc.ABC):

__slots__ = (
"_application",
"_asyncio_tasks",
"_command_invocation_mapping",
"_created_commands",
"_di",
"_error_handlers",
"_extensions",
"_localization",
"_menu_queues",
"_owner_ids",
"_registered_commands",
"_started",
Expand Down Expand Up @@ -168,7 +170,11 @@ def __init__(
self._error_handlers: dict[int, list[lb_types.ErrorHandler]] = {}
self._application: hikari.Application | None = None
self._extensions: set[str] = set()

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

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

self.di.registry_for(di_.Contexts.DEFAULT).register_value(hikari.api.RESTClient, self.rest)
self.di.registry_for(di_.Contexts.DEFAULT).register_value(Client, self)
Expand Down Expand Up @@ -196,6 +202,12 @@ def created_commands(self) -> Mapping[hikari.Snowflakeish, Collection[hikari.Par
"""
return self._created_commands

def _safe_create_task(self, coro: Coroutine[None, None, T]) -> asyncio.Task[T]:
task = asyncio.create_task(coro)
self._asyncio_tasks.add(task)
task.add_done_callback(lambda tsk: self._asyncio_tasks.remove(tsk))
return task

async def start(self, *_: t.Any) -> None:
"""
Starts the client. Ensures that commands are registered properly with the client, and that
Expand Down Expand Up @@ -792,11 +804,11 @@ async def sync_application_commands(self) -> None:
subcommand_option = await subcommand.to_command_option(
self.default_locale, self.localization_provider
)
all_commands[(builder.name, subcommand_or_subgroup_option.name, subcommand_option.name)] = (
all_commands[(builder.name, subcommand_or_subgroup_option.name, subcommand_option.name)] = ( # noqa: RUF031
subcommand
)
else:
all_commands[(builder.name, subcommand_or_subgroup_option.name)] = subcommand_or_subgroup
all_commands[(builder.name, subcommand_or_subgroup_option.name)] = subcommand_or_subgroup # noqa: RUF031
else:
all_commands = {(builder.name,): command}

Expand Down Expand Up @@ -992,11 +1004,20 @@ async def handle_application_command_interaction(self, interaction: hikari.Comma
LOGGER.debug("invoking command - %r", command._command_data.qualified_name)
await self._execute_command_context(context)

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

await asyncio.gather(*(q.put(interaction) for q in self._menu_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)


class GatewayEnabledClient(Client):
Expand Down Expand Up @@ -1163,7 +1184,13 @@ def build_rest_command_context(
command_cls: type[commands.CommandBase],
response_callback: Callable[[hikari.api.InteractionResponseBuilder], None],
) -> context_.Context:
return context_.RestContext(self, interaction, options, command_cls(), response_callback)
return context_.RestContext(
client=self,
interaction=interaction,
options=options,
command=command_cls(),
_initial_response_callback=response_callback,
)

async def handle_rest_application_command_interaction(
self, interaction: hikari.CommandInteraction
Expand Down
1 change: 0 additions & 1 deletion lightbulb/commands/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# api_reference_gen::ignore
# Copyright (c) 2023-present tandemdude
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down
37 changes: 37 additions & 0 deletions lightbulb/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2023-present tandemdude
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from lightbulb.components.base import *
from lightbulb.components.menus import *
from lightbulb.components.modals import *

__all__ = [
"BaseComponent",
"ChannelSelect",
"InteractiveButton",
"LinkButton",
"MentionableSelect",
"Menu",
"MenuContext",
"RoleSelect",
"Select",
"TextSelect",
"UserSelect",
]
41 changes: 41 additions & 0 deletions lightbulb/components/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2023-present tandemdude
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations

__all__ = ["BaseComponent"]

import abc
import typing as t

from hikari.api import special_endpoints

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


class BaseComponent(abc.ABC, t.Generic[RowT]):
__slots__ = ()

@property
@abc.abstractmethod
def custom_id(self) -> str: ...

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

0 comments on commit 6a6b58a

Please sign in to comment.