Source code for lightbulb.help_command

# -*- 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__ = ["BaseHelpCommand", "DefaultHelpCommand", "filter_commands"]

import abc
import collections
import typing as t

from lightbulb import commands
from lightbulb import errors
from lightbulb.utils import nav

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


[docs]async def filter_commands( cmds: t.Sequence[commands.base.Command], context: context_.base.Context ) -> t.Sequence[commands.base.Command]: """ Evaluates the checks for each command provided, removing any that the checks fail for. This effectively removes any commands from the given collection that could not be invoked under the given context. This will also remove any commands with the ``hidden`` attribute set to ``True``. Args: cmds (Sequence[:obj:`~.commands.base.Command`]): Commands to filter. context (:obj:`~.context.base.Context`): Context to filter the commands under. Returns: Sequence[:obj:`~.commands.base.Command`]: Filtered commands. """ new_cmds = [] for cmd in cmds: if cmd.hidden: continue try: await cmd.evaluate_checks(context) except errors.CheckFailure: continue new_cmds.append(cmd) return new_cmds
[docs]class BaseHelpCommand(abc.ABC): """ Base class for auto-generated help commands. Args: app (:obj:`~.app.BotApp`): The ``BotApp`` instance that the help command is registered to. """ __slots__ = ("app",) def __init__(self, app: app_.BotApp) -> None: self.app: app_.BotApp = app """The ``BotApp`` instance the help command is registered to.""" @property def bot(self) -> t.Optional[app_.BotApp]: """Alias for :obj:`~BaseHelpCommand.app`""" return self.app
[docs] async def send_help(self, context: context_.base.Context, obj: t.Optional[str]) -> None: """ Resolve the given object and send the help text for it to the given context. Help resolution order: - Prefix command - Slash command - Message command - User command - Plugin Args: context (:obj:`~.context.base.Context`): Context to send help to. obj (:obj:`str`): String representation of the object to send help for. """ await self._send_help(context, obj)
async def _send_help(self, context: context_.base.Context, obj: t.Optional[str]) -> None: if obj is None: await self.send_bot_help(context) return p_cmd = self.app.get_prefix_command(obj) if p_cmd is not None and not p_cmd.hidden: if isinstance(p_cmd, (commands.prefix.PrefixCommandGroup, commands.prefix.PrefixSubGroup)): await self.send_group_help(context, p_cmd) return await self.send_command_help(context, p_cmd) return s_cmd = self.app.get_slash_command(obj) if s_cmd is not None and not s_cmd.hidden: if isinstance(s_cmd, (commands.slash.SlashCommandGroup, commands.slash.SlashSubGroup)): await self.send_group_help(context, s_cmd) return await self.send_command_help(context, s_cmd) return m_cmd = self.app.get_message_command(obj) if m_cmd is not None and not m_cmd.hidden: await self.send_command_help(context, m_cmd) return u_cmd = self.app.get_user_command(obj) if u_cmd is not None and not u_cmd.hidden: await self.send_command_help(context, u_cmd) return plugin = self.app.get_plugin(obj) if plugin is not None: await self.send_plugin_help(context, plugin) return await self.object_not_found(context, obj)
[docs] @abc.abstractmethod async def send_bot_help(self, context: context_.base.Context) -> None: """ Sends an overall help message for the bot. This is called when no object is provided when the help command is invoked. Args: context (:obj:`~.context.base.Context`): Context to send help to. Returns: ``None`` """ ...
[docs] @abc.abstractmethod async def send_command_help(self, context: context_.base.Context, command: commands.base.Command) -> None: """ Sends a help message for the given command. Args: context (:obj:`~.context.base.Context`): Context to send help to. command (:obj:`~.commands.base.Command`): Command to send help for. Returns: ``None`` """ ...
[docs] @abc.abstractmethod async def send_group_help( self, context: context_.base.Context, group: t.Union[ commands.prefix.PrefixCommandGroup, commands.prefix.PrefixSubGroup, commands.slash.SlashCommandGroup, commands.slash.SlashSubGroup, ], ) -> None: """ Sends a help message for the given command group. Args: context (:obj:`~.context.base.Context`): Context to send help to. group: Command group to send help for. Returns: ``None`` """ ...
[docs] @abc.abstractmethod async def send_plugin_help(self, context: context_.base.Context, plugin: plugins.Plugin) -> None: """ Sends a help message for the given plugin. Args: context (:obj:`~.context.base.Context`): Context to send help to. plugin (:obj:`~.plugins.Plugin`): Plugin to send help for. Returns: ``None`` """ ...
[docs] async def object_not_found(self, context: context_.base.Context, obj: str) -> None: """ Method called when no object could be resolved from the given name. Args: context (:obj:`~.context.base.Context`): Context to send help to. obj (:obj:`str`): String that the help command was invoked with but that could not be resolved into an object. Returns: ``None`` """ await context.respond(f"No command or category with the name `{obj}` could be found.")
[docs]class DefaultHelpCommand(BaseHelpCommand): """ An implementation of the :obj:`~BaseHelpCommand` that the bot uses by default. """ @staticmethod async def _get_command_plugin_map( cmd_map: t.Mapping[str, commands.base.Command], context: context_.base.Context ) -> t.Dict[t.Optional[plugins.Plugin], t.List[commands.base.Command]]: out = collections.defaultdict(list) for cmd in cmd_map.values(): if await filter_commands([cmd], context): out[cmd.plugin].append(cmd) return out @staticmethod def _add_cmds_to_plugin_pages( pages: t.MutableMapping[t.Optional[plugins.Plugin], t.List[str]], cmds: t.Mapping[t.Optional[plugins.Plugin], t.List[commands.base.Command]], header: str, ) -> None: for plugin, cmds in cmds.items(): pages[plugin].append(f"== {header} Commands") for cmd in set(cmds): pages[plugin].append(f"- {cmd.name} - {cmd.description}")
[docs] async def send_bot_help(self, context: context_.base.Context) -> None: pages = [] lines = [ ">>> ```adoc", "==== Bot Help ====", "", f"For more information: {context.prefix}help [command|category]", "", "==== Categories ====", ] p_commands = await self._get_command_plugin_map(self.app._prefix_commands, context) s_commands = await self._get_command_plugin_map(self.app._slash_commands, context) m_commands = await self._get_command_plugin_map(self.app._message_commands, context) u_commands = await self._get_command_plugin_map(self.app._user_commands, context) plugin_pages: t.MutableMapping[t.Optional[plugins.Plugin], t.List[str]] = collections.defaultdict(list) self._add_cmds_to_plugin_pages(plugin_pages, p_commands, "Prefix") self._add_cmds_to_plugin_pages(plugin_pages, s_commands, "Slash") self._add_cmds_to_plugin_pages(plugin_pages, m_commands, "Message") self._add_cmds_to_plugin_pages(plugin_pages, u_commands, "User") for plugin, page in plugin_pages.items(): if plugin is not None: lines.append(f"- {plugin.name}") pages.append( "\n".join( [ ">>> ```adoc", f"==== {plugin.name if plugin is not None else 'Uncategorised'} ====", (f"{plugin.description}\n" if plugin.description else "No description provided\n") if plugin is not None else "", *page, "```", ] ) ) lines.append("```") pages.insert(0, "\n".join(lines)) navigator = nav.ButtonNavigator(pages) await navigator.run(context)
[docs] async def send_command_help(self, context: context_.base.Context, command: commands.base.Command) -> None: long_help = command.get_help(context) prefix = ( context.prefix if isinstance(command, commands.prefix.PrefixCommand) else "/" if isinstance(command, commands.slash.SlashCommand) else "\N{THREE BUTTON MOUSE}" ) lines = [ ">>> ```adoc", "==== Command Help ====", f"{command.name} - {command.description}", "", f"Usage: {prefix}{command.signature}", "", long_help if long_help else "No additional details provided.", "```", ] await context.respond("\n".join(lines))
[docs] async def send_group_help( self, context: context_.base.Context, group: t.Union[ commands.prefix.PrefixCommandGroup, commands.prefix.PrefixSubGroup, commands.slash.SlashCommandGroup, commands.slash.SlashSubGroup, ], ) -> None: long_help = group.get_help(context) prefix = ( context.prefix if isinstance(group, commands.prefix.PrefixCommand) else "/" if isinstance(group, commands.slash.SlashCommand) else "\N{THREE BUTTON MOUSE}" ) usages = list( filter( None, [ f"{prefix}{group.signature}" if isinstance(group, commands.prefix.PrefixCommand) else None, f"{prefix}{group.qualname} [subcommand]", ], ) ) usages[0] = f"Usage: {usages[0]}" if len(usages) > 1: usages[1] = f"Or: {usages[1]}" lines = [ ">>> ```adoc", "==== Group Help ====", f"{group.name} - {group.description}", "", "\n".join(usages), "", long_help if long_help else "No additional details provided.", "", ] if group._subcommands: subcommands = await filter_commands(group._subcommands.values(), context) # type: ignore lines.append("== Subcommands") for cmd in set(subcommands): lines.append(f"- {cmd.name} - {cmd.description}") lines.append("```") await context.respond("\n".join(lines))
[docs] async def send_plugin_help(self, context: context_.base.Context, plugin: plugins.Plugin) -> None: lines = [ ">>> ```adoc", "==== Category Help ====", f"{plugin.name} - {plugin.description or 'No description provided'}", "", ] p_cmds, s_cmds, m_cmds, u_cmds = [], [], [], [] all_commands = await filter_commands(plugin._all_commands, context) for cmd in all_commands: if isinstance(cmd, commands.prefix.PrefixCommand): p_cmds.append(cmd) elif isinstance(cmd, commands.slash.SlashCommand): s_cmds.append(cmd) elif isinstance(cmd, commands.message.MessageCommand): m_cmds.append(cmd) elif isinstance(cmd, commands.user.UserCommand): u_cmds.append(cmd) cmds: t.List[t.Tuple[t.Sequence[commands.base.Command], str]] = [ (p_cmds, "Prefix"), (s_cmds, "Slash"), (m_cmds, "Message"), (u_cmds, "User"), ] for cmd_list, header in cmds: if cmd_list: lines.append(f"== {header} Commands") for cmd in set(cmd_list): lines.append(f"- {cmd.name} - {cmd.description}") lines.append("```") await context.respond("\n".join(lines))