Source code for lightbulb.commands.slash

# -*- coding: utf-8 -*-
# Copyright © tandemdude 2020-present
#
# This file is part of Lightbulb.
#
# Lightbulb is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Lightbulb is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Lightbulb. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations

__all__ = ["SlashCommand", "SlashCommandGroup", "SlashGroupMixin", "SlashSubGroup", "SlashSubCommand"]

import abc
import re
import typing as t

import hikari

from lightbulb import context as context_
from lightbulb import errors
from lightbulb.commands import base

if t.TYPE_CHECKING:
    from lightbulb import app as app_
    from lightbulb import plugins

COMMAND_NAME_REGEX: re.Pattern[str] = re.compile(r"^[\w-]{1,32}$", re.U)


[docs] class SlashGroupMixin(abc.ABC): __slots__ = () _plugin: t.Optional[plugins.Plugin] _subcommands: t.Dict[str, t.Union[SlashSubGroup, SlashSubCommand]] @property @abc.abstractmethod def name(self) -> str: ... def create_subcommands( self, raw_cmds: t.Sequence[base.CommandLike], app: app_.BotApp, allowed_types: t.Union[t.Tuple[t.Type[SlashSubCommand], t.Type[SlashSubGroup]], t.Type[SlashSubCommand]], ) -> None: for raw_cmd in raw_cmds: impls: t.List[t.Type[base.Command]] = getattr(raw_cmd.callback, "__cmd_types__", []) for impl in impls: if issubclass(impl, allowed_types): cmd = impl(app, raw_cmd) assert isinstance(cmd, (SlashSubCommand, SlashSubGroup)) cmd.parent = self # type: ignore cmd.plugin = self.plugin # type: ignore if cmd.name in self._subcommands: raise errors.CommandAlreadyExists( f"A prefix subcommand with name or alias {cmd.name!r} " + f"already exists for group {self.name!r}" ) self._subcommands[cmd.name] = cmd def recreate_subcommands(self, raw_cmds: t.Sequence[base.CommandLike], app: app_.BotApp) -> None: self._subcommands.clear() self.create_subcommands( raw_cmds, app, SlashSubCommand if isinstance(self, SlashSubGroup) else (SlashSubCommand, SlashSubGroup) ) async def _invoke_subcommand(self, context: context_.base.Context) -> None: assert isinstance(context, context_.slash.SlashContext) cmd_option = context._raw_options[0] context._raw_options = cmd_option.options or [] # Replace the invoked command prematurely so that _parse_options uses the correct command options context._invoked = self._subcommands[cmd_option.name] # Reparse the options for the subcommand context._parse_options(cmd_option.options) # Ensure we call _maybe_defer await context._maybe_defer() # Invoke the subcommand await context._invoked.invoke(context)
[docs] def get_subcommand(self, name: str) -> t.Optional[t.Union[SlashSubGroup, SlashSubCommand]]: """Get the group's subcommand with the given name.""" return self._subcommands.get(name)
@property def subcommands(self) -> t.Dict[str, t.Union[SlashSubGroup, SlashSubCommand]]: """Mapping of command name to command object containing the group's subcommands.""" return self._subcommands def _set_plugin(self, pl: plugins.Plugin) -> None: self._plugin = pl # type: ignore[misc] for command in self._subcommands.values(): if isinstance(command, SlashGroupMixin): command._set_plugin(pl) else: command.plugin = pl
[docs] class SlashCommand(base.ApplicationCommand): """ An implementation of :obj:`~.commands.base.Command` representing a slash command. See the `API Documentation <https://discord.com/developers/docs/interactions/application-commands#slash-commands>`_. """ __slots__ = ()
[docs] def as_create_kwargs(self) -> t.Dict[str, t.Any]: sorted_opts = sorted(self.options.values(), key=lambda o: int(o.required), reverse=True) return { "type": hikari.CommandType.SLASH, "name": self.name, "description": self.description, "options": [o.as_application_command_option() for o in sorted_opts], "name_localizations": self.name_localizations, "description_localizations": self.description_localizations, }
def _validate_attributes(self) -> None: if not COMMAND_NAME_REGEX.fullmatch(self.name) or self.name != self.name.lower(): raise ValueError( f"Slash command {self.name!r}: name must match regex '^[\\w-]{1,32}$' and be all lowercase" ) from None if len(self.description) < 1 or len(self.description) > 100: raise ValueError(f"Slash command {self.name!r}: description must be from 1-100 characters long") from None if len(self.options) > 25: raise ValueError(f"Slash command {self.name!r}: can at most have 25 options") from None
[docs] class SlashSubCommand(SlashCommand, base.SubCommandTrait): """ Class representing a slash subcommand. """ __slots__ = () @property def qualname(self) -> str: assert self.parent is not None return f"{self.parent.qualname} {self.name}" def as_option(self) -> hikari.CommandOption: sorted_opts = sorted(self.options.values(), key=lambda o: int(o.required), reverse=True) return hikari.CommandOption( type=hikari.OptionType.SUB_COMMAND, name=self.name, description=self.description, is_required=False, options=[o.as_application_command_option() for o in sorted_opts], name_localizations=self.name_localizations, description_localizations=self.description_localizations, )
[docs] class SlashSubGroup(SlashCommand, SlashGroupMixin, base.SubCommandTrait): """ Class representing a slash subgroup of commands. """ __slots__ = ("_raw_subcommands", "_subcommands") def __init__(self, app: app_.BotApp, initialiser: base.CommandLike) -> None: super().__init__(app, initialiser) self._raw_subcommands = initialiser.subcommands initialiser.subcommands = ( initialiser.subcommands.add_parent(self) # type: ignore if isinstance(initialiser.subcommands, base._SubcommandListProxy) # type: ignore else base._SubcommandListProxy(initialiser.subcommands, parent=self) ) # Just to keep mypy happy we leave SlashSubGroup here self._subcommands: t.Dict[str, t.Union[SlashSubGroup, SlashSubCommand]] = {} self.create_subcommands(self._raw_subcommands, app, SlashSubCommand) @property def qualname(self) -> str: assert self.parent is not None return f"{self.parent.qualname} {self.name}" def as_option(self) -> hikari.CommandOption: return hikari.CommandOption( type=hikari.OptionType.SUB_COMMAND_GROUP, name=self.name, description=self.description, is_required=False, options=[c.as_option() for c in self._subcommands.values()], name_localizations=self.name_localizations, description_localizations=self.description_localizations, )
[docs] async def invoke(self, context: context_.base.Context, **_: t.Any) -> None: await self._invoke_subcommand(context)
def _validate_attributes(self) -> None: super()._validate_attributes() if len(self._subcommands) > 25: raise ValueError(f"Slash command {self.name!r}: group can have at most 25 subcommands") from None def _set_plugin(self, pl: plugins.Plugin) -> None: SlashGroupMixin._set_plugin(self, pl)
[docs] class SlashCommandGroup(SlashCommand, SlashGroupMixin): """ Class representing a slash command group. """ __slots__ = ("_raw_subcommands", "_subcommands") def __init__(self, app: app_.BotApp, initialiser: base.CommandLike) -> None: super().__init__(app, initialiser) self._raw_subcommands = initialiser.subcommands initialiser.subcommands = ( initialiser.subcommands.add_parent(self) # type: ignore if isinstance(initialiser.subcommands, base._SubcommandListProxy) # type: ignore else base._SubcommandListProxy(initialiser.subcommands, parent=self) ) self._subcommands: t.Dict[str, t.Union[SlashSubGroup, SlashSubCommand]] = {} self.create_subcommands(self._raw_subcommands, app, (SlashSubCommand, SlashSubGroup))
[docs] async def invoke(self, context: context_.base.Context, **_: t.Any) -> None: await self._invoke_subcommand(context)
[docs] def as_create_kwargs(self) -> t.Dict[str, t.Any]: return { "type": hikari.CommandType.SLASH, "name": self.name, "description": self.description, "options": [c.as_option() for c in self._subcommands.values()], "name_localizations": self.name_localizations, "description_localizations": self.description_localizations, }
def _validate_attributes(self) -> None: super()._validate_attributes() if len(self._subcommands) > 25: raise ValueError(f"Slash command {self.name!r}: group can have at most 25 subcommands") from None def _set_plugin(self, pl: plugins.Plugin) -> None: SlashGroupMixin._set_plugin(self, pl)