# -*- 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__ = ["Group", "SubGroup"]
import abc
import dataclasses
import typing as t
import hikari
from lightbulb.commands import commands
from lightbulb.commands import utils
if t.TYPE_CHECKING:
from collections.abc import Callable
from collections.abc import Mapping
from collections.abc import Sequence
from lightbulb import localization
CommandT = t.TypeVar("CommandT", bound=type["commands.CommandBase"])
SubGroupCommandMappingT = dict[str, type["commands.CommandBase"]]
GroupCommandMappingT = dict[str, t.Union["SubGroup", type["commands.CommandBase"]]]
class GroupMixin(abc.ABC):
"""Base class for application command groups."""
__slots__ = ()
_commands: SubGroupCommandMappingT | GroupCommandMappingT
@t.overload
def register(self) -> Callable[[CommandT], CommandT]: ...
@t.overload
def register(self, command: CommandT) -> CommandT: ...
def register(self, command: CommandT | None = None) -> CommandT | Callable[[CommandT], CommandT]:
"""
Register a command as a subcommand for this group. Can be used as a first or second order decorator,
or called with the command to register.
Args:
command: The command to register to the group as a subcommand.
Returns:
The passed command, with the parent set.
Example:
.. code-block:: python
group = lightbulb.Group("name", "description")
# valid
@group.register # or @group.register()
class Example(
lightbulb.SlashCommand,
...
):
...
# also valid
group.register(Example)
"""
if command is not None:
self._commands[command._command_data.name] = command
command._command_data.parent = self # type: ignore[reportGeneralTypeIssues]
return command
def _inner(_command: CommandT) -> CommandT:
return self.register(_command)
return _inner
[docs]
@dataclasses.dataclass(slots=True, frozen=True)
class SubGroup(GroupMixin):
"""
Dataclass representing a slash command subgroup.
Warning:
This **should not** be instantiated manually - you should instead create one using :meth:`Group.subgroup`.
"""
name: str
"""The name of the subgroup."""
description: str
"""The description of the subgroup."""
localize: bool = dataclasses.field(repr=False)
"""Whether the group name and description should be localized."""
parent: Group = dataclasses.field(repr=False)
"""The parent group of the subgroup."""
_commands: SubGroupCommandMappingT = dataclasses.field(init=False, hash=False, repr=False, default_factory=dict) # type: ignore[reportUnknownVariableType]
@property
def _command_data(self) -> commands.CommandData:
cdata = commands.CommandData(
hikari.CommandType.SLASH,
self.name,
self.description,
self.localize,
self.parent.nsfw,
self.parent.integration_types,
self.parent.contexts,
self.parent.default_member_permissions,
[],
{},
"",
)
cdata.parent = self.parent
return cdata
@property
def subcommands(self) -> SubGroupCommandMappingT:
"""The subcommands of this subgroup."""
return self._commands
[docs]
async def to_command_option(
self, default_locale: hikari.Locale, localization_provider: localization.LocalizationProvider
) -> hikari.CommandOption:
"""
Convert the subgroup into a subgroup command option.
Returns:
:obj:`hikari.CommandOption`: The subgroup option for this subgroup.
"""
name, description = self.name, self.description
name_localizations: Mapping[hikari.Locale, str] = {}
description_localizations: Mapping[hikari.Locale, str] = {}
if self.localize:
(
name,
description,
name_localizations,
description_localizations,
) = await utils.localize_name_and_description(name, description, default_locale, localization_provider)
return hikari.CommandOption(
type=hikari.OptionType.SUB_COMMAND_GROUP,
name=name,
name_localizations=name_localizations, # type: ignore[reportArgumentType]
description=description,
description_localizations=description_localizations, # type: ignore[reportArgumentType]
options=[
await command.to_command_option(default_locale, localization_provider)
for command in self._commands.values()
],
)
[docs]
@dataclasses.dataclass(slots=True, frozen=True)
class Group(GroupMixin):
"""
Dataclass representing a slash command group.
Note:
If ``localize`` is :obj:`True`, then ``name`` and ``description`` will instead be
interpreted as localization keys from which the actual name and description will be retrieved from.
"""
name: str
"""The name of the group."""
description: str
"""The description of the group."""
localize: bool = dataclasses.field(repr=False, default=False)
"""Whether the group name and description should be localized."""
nsfw: bool = dataclasses.field(repr=False, default=False)
"""Whether the group should be marked as nsfw. Defaults to :obj:`False`."""
integration_types: hikari.UndefinedOr[Sequence[hikari.ApplicationIntegrationType]] = dataclasses.field(
hash=False, repr=False, default=hikari.UNDEFINED
)
"""Installation contexts where the command is available. Only affects global commands."""
contexts: hikari.UndefinedOr[Sequence[hikari.ApplicationContextType]] = dataclasses.field(
hash=False, repr=False, default=hikari.UNDEFINED
)
"""Interaction contexts where the command can be used. Only affects global commands."""
default_member_permissions: hikari.UndefinedOr[hikari.Permissions] = dataclasses.field(
repr=False, default=hikari.UNDEFINED
)
"""The default permissions required to use the group in a guild."""
extension: str | None = dataclasses.field(init=False, repr=False, default=None)
"""The extensions that the command's loader was loaded from, or :obj:`None` if not applicable."""
_commands: GroupCommandMappingT = dataclasses.field(init=False, hash=False, repr=False, default_factory=dict) # type: ignore[reportUnknownVariableType]
@property
def _command_data(self) -> commands.CommandData:
cdata = commands.CommandData(
hikari.CommandType.SLASH,
self.name,
self.description,
self.localize,
self.nsfw,
self.integration_types,
self.contexts,
self.default_member_permissions,
[],
{},
"",
)
cdata.extension = self.extension
return cdata
@property
def subcommands(self) -> GroupCommandMappingT:
"""The subcommands and subgroups of this group."""
return self._commands
[docs]
def subgroup(self, name: str, description: str, *, localize: bool = False) -> SubGroup:
"""
Create a new subgroup as a child of this group.
Args:
name: The name of the subgroup.
description: The description of the subgroup.
localize: Whether to localize the group's name and description. If :obj:`true`,
then the ``name`` and ``description`` arguments will instead be interpreted as localization keys from
which the actual name and description will be retrieved from. Defaults to :obj:`False`.
Returns:
:obj:`~SubGroup`: The created subgroup.
"""
new = SubGroup(name=name, description=description, localize=localize, parent=self)
self._commands[name] = new
return new
[docs]
async def as_command_builder(
self, default_locale: hikari.Locale, localization_provider: localization.LocalizationProvider
) -> hikari.api.CommandBuilder:
"""
Convert the group into a hikari command builder object.
Returns:
:obj:`hikari.api.CommandBuilder`: The builder object for this group.
"""
name, description = self.name, self.description
name_localizations: Mapping[hikari.Locale, str] = {}
description_localizations: Mapping[hikari.Locale, str] = {}
if self.localize:
(
name,
description,
name_localizations,
description_localizations,
) = await utils.localize_name_and_description(name, description, default_locale, localization_provider)
bld = (
hikari.impl.SlashCommandBuilder(name=name, description=description)
.set_name_localizations(name_localizations) # type: ignore[reportArgumentType]
.set_description_localizations(description_localizations) # type: ignore[reportArgumentType]
.set_is_nsfw(self.nsfw)
.set_default_member_permissions(self.default_member_permissions)
.set_integration_types(self.integration_types)
.set_context_types(self.contexts)
)
for command_or_group in self._commands.values():
option = await command_or_group.to_command_option(default_locale, localization_provider)
bld.add_option(option)
return bld