aboutsummaryrefslogtreecommitdiff
path: root/discord/ext/commands/help.py
diff options
context:
space:
mode:
Diffstat (limited to 'discord/ext/commands/help.py')
-rw-r--r--discord/ext/commands/help.py1144
1 files changed, 1144 insertions, 0 deletions
diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py
new file mode 100644
index 00000000..3f956e55
--- /dev/null
+++ b/discord/ext/commands/help.py
@@ -0,0 +1,1144 @@
+# -*- coding: utf-8 -*-
+
+"""
+The MIT License (MIT)
+
+Copyright (c) 2015-2019 Rapptz
+
+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.
+"""
+
+import itertools
+import functools
+import inspect
+import re
+import discord.utils
+
+from .core import Group, Command
+from .errors import CommandError
+
+__all__ = [
+ 'Paginator',
+ 'HelpCommand',
+ 'DefaultHelpCommand',
+ 'MinimalHelpCommand',
+]
+
+# help -> shows info of bot on top/bottom and lists subcommands
+# help command -> shows detailed info of command
+# help command <subcommand chain> -> same as above
+
+# <description>
+
+# <command signature with aliases>
+
+# <long doc>
+
+# Cog:
+# <command> <shortdoc>
+# <command> <shortdoc>
+# Other Cog:
+# <command> <shortdoc>
+# No Category:
+# <command> <shortdoc>
+
+# Type <prefix>help command for more info on a command.
+# You can also type <prefix>help category for more info on a category.
+
+class Paginator:
+ """A class that aids in paginating code blocks for Discord messages.
+
+ Attributes
+ -----------
+ prefix: Optional[:class:`str`]
+ The prefix inserted to every page. e.g. three backticks.
+ suffix: Optional[:class:`str`]
+ The suffix appended at the end of every page. e.g. three backticks.
+ max_size: :class:`int`
+ The maximum amount of codepoints allowed in a page.
+ """
+ def __init__(self, prefix='```', suffix='```', max_size=2000):
+ self.prefix = prefix
+ self.suffix = suffix
+ self.max_size = max_size - (0 if suffix is None else len(suffix))
+ self.clear()
+
+ def clear(self):
+ """Clears the paginator to have no pages."""
+ if self.prefix is not None:
+ self._current_page = [self.prefix]
+ self._count = len(self.prefix) + 1 # prefix + newline
+ else:
+ self._current_page = []
+ self._count = 0
+ self._pages = []
+
+ @property
+ def _prefix_len(self):
+ return len(self.prefix) if self.prefix else 0
+
+ def add_line(self, line='', *, empty=False):
+ """Adds a line to the current page.
+
+ If the line exceeds the :attr:`max_size` then an exception
+ is raised.
+
+ Parameters
+ -----------
+ line: str
+ The line to add.
+ empty: bool
+ Indicates if another empty line should be added.
+
+ Raises
+ ------
+ RuntimeError
+ The line was too big for the current :attr:`max_size`.
+ """
+ max_page_size = self.max_size - self._prefix_len - 2
+ if len(line) > max_page_size:
+ raise RuntimeError('Line exceeds maximum page size %s' % (max_page_size))
+
+ if self._count + len(line) + 1 > self.max_size:
+ self.close_page()
+
+ self._count += len(line) + 1
+ self._current_page.append(line)
+
+ if empty:
+ self._current_page.append('')
+ self._count += 1
+
+ def close_page(self):
+ """Prematurely terminate a page."""
+ if self.suffix is not None:
+ self._current_page.append(self.suffix)
+ self._pages.append('\n'.join(self._current_page))
+
+ if self.prefix is not None:
+ self._current_page = [self.prefix]
+ self._count = len(self.prefix) + 1 # prefix + newline
+ else:
+ self._current_page = []
+ self._count = 0
+
+ @property
+ def pages(self):
+ """Returns the rendered list of pages."""
+ # we have more than just the prefix in our current page
+ if len(self._current_page) > 1:
+ self.close_page()
+ return self._pages
+
+ def __repr__(self):
+ fmt = '<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>'
+ return fmt.format(self)
+
+def _not_overriden(f):
+ f.__help_command_not_overriden__ = True
+ return f
+
+class _HelpCommandImpl(Command):
+ def __init__(self, inject, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._injected = inject
+
+ on_error = inject.on_help_command_error
+ try:
+ on_error.__help_command_not_overriden__
+ except AttributeError:
+ # overridden
+ self.on_error = on_error
+
+ async def _parse_arguments(self, ctx):
+ # Make the parser think we don't have a cog so it doesn't
+ # inject the parameter into `ctx.args`.
+ original_cog = self.cog
+ self.cog = None
+ try:
+ await super()._parse_arguments(ctx)
+ finally:
+ self.cog = original_cog
+
+ async def _on_error_cog_implementation(self, dummy, ctx, error):
+ await self._injected.on_help_command_error(ctx, error)
+
+ @property
+ def clean_params(self):
+ result = self.params.copy()
+ try:
+ result.popitem(last=False)
+ except Exception:
+ raise ValueError('Missing context parameter') from None
+ else:
+ return result
+
+ def _inject_into_cog(self, cog):
+ # Warning: hacky
+
+ # Make the cog think that get_commands returns this command
+ # as well if we inject it without modifying __cog_commands__
+ # since that's used for the injection and ejection of cogs.
+ def wrapped_get_commands(*, _original=cog.get_commands):
+ ret = _original()
+ ret.append(self)
+ return ret
+
+ # Ditto here
+ def wrapped_walk_commands(*, _original=cog.walk_commands):
+ yield from _original()
+ yield self
+
+ functools.update_wrapper(wrapped_get_commands, cog.get_commands)
+ functools.update_wrapper(wrapped_walk_commands, cog.walk_commands)
+ cog.get_commands = wrapped_get_commands
+ cog.walk_commands = wrapped_walk_commands
+ self.cog = cog
+
+ on_error = self._injected.on_help_command_error
+ try:
+ on_error.__help_command_not_overriden__
+ except AttributeError:
+ # overridden so let's swap it with our cog specific implementation
+ self.on_error = self._on_error_cog_implementation
+
+ def _eject_cog(self):
+ if self.cog is None:
+ return
+
+ # revert back into their original methods
+ cog = self.cog
+ cog.get_commands = cog.get_commands.__wrapped__
+ cog.walk_commands = cog.walk_commands.__wrapped__
+ self.cog = None
+
+ on_error = self._injected.on_help_command_error
+ try:
+ on_error.__help_command_not_overriden__
+ except AttributeError:
+ # overridden so let's swap it with our cog specific implementation
+ self.on_error = self._on_error_cog_implementation
+
+class HelpCommand:
+ r"""The base implementation for help command formatting.
+
+ Attributes
+ ------------
+ context: Optional[:class:`Context`]
+ The context that invoked this help formatter. This is generally set after
+ the help command assigned, :func:`command_callback`\, has been called.
+ show_hidden: :class:`bool`
+ Specifies if hidden commands should be shown in the output.
+ Defaults to ``False``.
+ verify_checks: :class:`bool`
+ Specifies if commands should have their :attr:`.Command.checks` called
+ and verified. Defaults to ``True``.
+ command_attrs: :class:`dict`
+ A dictionary of options to pass in for the construction of the help command.
+ This allows you to change the command behaviour without actually changing
+ the implementation of the command. The attributes will be the same as the
+ ones passed in the :class:`.Command` constructor.
+ """
+
+ MENTION_TRANSFORMS = {
+ '@everyone': '@\u200beveryone',
+ '@here': '@\u200bhere',
+ r'<@!?[0-9]{17,22}>': '@deleted-user',
+ r'<@&[0-9]{17,22}>': '@deleted-role'
+ }
+
+ MENTION_PATTERN = re.compile('|'.join(MENTION_TRANSFORMS.keys()))
+
+ def __init__(self, **options):
+ self.show_hidden = options.pop('show_hidden', False)
+ self.verify_checks = options.pop('verify_checks', True)
+ self.command_attrs = attrs = options.pop('command_attrs', {})
+ attrs.setdefault('name', 'help')
+ attrs.setdefault('help', 'Shows this message')
+ self.context = None
+ self._command_impl = None
+
+ def _add_to_bot(self, bot):
+ command = _HelpCommandImpl(self, self.command_callback, **self.command_attrs)
+ bot.add_command(command)
+ self._command_impl = command
+
+ def _remove_from_bot(self, bot):
+ bot.remove_command(self._command_impl.name)
+ self._command_impl._eject_cog()
+ self._command_impl = None
+
+ @property
+ def clean_prefix(self):
+ """The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
+ user = self.context.guild.me if self.context.guild else self.context.bot.user
+ # this breaks if the prefix mention is not the bot itself but I
+ # consider this to be an *incredibly* strange use case. I'd rather go
+ # for this common use case rather than waste performance for the
+ # odd one.
+ return self.context.prefix.replace(user.mention, '@' + user.display_name)
+
+ def get_command_signature(self, command):
+ """Retrieves the signature portion of the help page.
+
+ Parameters
+ ------------
+ command: :class:`Command`
+ The command to get the signature of.
+
+ Returns
+ --------
+ :class:`str`
+ The signature for the command.
+ """
+
+ parent = command.full_parent_name
+ if len(command.aliases) > 0:
+ aliases = '|'.join(command.aliases)
+ fmt = '[%s|%s]' % (command.name, aliases)
+ if parent:
+ fmt = parent + ' ' + fmt
+ alias = fmt
+ else:
+ alias = command.name if not parent else parent + ' ' + command.name
+
+ return '%s%s %s' % (self.clean_prefix, alias, command.signature)
+
+ def remove_mentions(self, string):
+ """Removes mentions from the string to prevent abuse.
+
+ This includes ``@everyone``, ``@here``, member mentions and role mentions.
+ """
+
+ def replace(obj, *, transforms=self.MENTION_TRANSFORMS):
+ return transforms.get(obj.group(0), '@invalid')
+
+ return self.MENTION_PATTERN.sub(replace, string)
+
+ @property
+ def cog(self):
+ """A property for retrieving or setting the cog for the help command.
+
+ When a cog is set for the help command, it is as-if the help command
+ belongs to that cog. All cog special methods will apply to the help
+ command and it will be automatically unset on unload.
+
+ To unbind the cog from the help command, you can set it to ``None``.
+
+ Returns
+ --------
+ Optional[:class:`Cog`]
+ The cog that is currently set for the help command.
+ """
+ return self._command_impl.cog
+
+ @cog.setter
+ def cog(self, cog):
+ # Remove whatever cog is currently valid, if any
+ self._command_impl._eject_cog()
+
+ # If a new cog is set then inject it.
+ if cog is not None:
+ self._command_impl._inject_into_cog(cog)
+
+ def command_not_found(self, string):
+ """|maybecoro|
+
+ A method called when a command is not found in the help command.
+ This is useful to override for i18n.
+
+ Defaults to ``No command called {0} found.``
+
+ Parameters
+ ------------
+ string: :class:`str`
+ The string that contains the invalid command. Note that this has
+ had mentions removed to prevent abuse.
+
+ Returns
+ ---------
+ :class:`str`
+ The string to use when a command has not been found.
+ """
+ return 'No command called "{}" found.'.format(string)
+
+ def subcommand_not_found(self, command, string):
+ """|maybecoro|
+
+ A method called when a command did not have a subcommand requested in the help command.
+ This is useful to override for i18n.
+
+ Defaults to either:
+
+ - ``'Command "{command.qualified_name}" has no subcommands.'``
+ - If there is no subcommand in the ``command`` parameter.
+ - ``'Command "{command.qualified_name}" has no subcommand named {string}'``
+ - If the ``command`` parameter has subcommands but not one named ``string``.
+
+ Parameters
+ ------------
+ command: :class:`Command`
+ The command that did not have the subcommand requested.
+ string: :class:`str`
+ The string that contains the invalid subcommand. Note that this has
+ had mentions removed to prevent abuse.
+
+ Returns
+ ---------
+ :class:`str`
+ The string to use when the command did not have the subcommand requested.
+ """
+ if isinstance(command, Group) and len(command.all_commands) > 0:
+ return 'Command "{0.qualified_name}" has no subcommand named {1}'.format(command, string)
+ return 'Command "{0.qualified_name}" has no subcommands.'.format(command)
+
+ async def filter_commands(self, commands, *, sort=False, key=None):
+ """|coro|
+
+ Returns a filtered list of commands and optionally sorts them.
+
+ This takes into account the :attr:`verify_checks` and :attr:`show_hidden`
+ attributes.
+
+ Parameters
+ ------------
+ commands: Iterable[:class:`Command`]
+ An iterable of commands that are getting filtered.
+ sort: :class:`bool`
+ Whether to sort the result.
+ key: Optional[Callable[:class:`Command`, Any]]
+ An optional key function to pass to :func:`py:sorted` that
+ takes a :class:`Command` as its sole parameter. If ``sort`` is
+ passed as ``True`` then this will default as the command name.
+
+ Returns
+ ---------
+ List[:class:`Command`]
+ A list of commands that passed the filter.
+ """
+
+ if sort and key is None:
+ key = lambda c: c.name
+
+ iterator = commands if self.show_hidden else filter(lambda c: not c.hidden, commands)
+
+ if not self.verify_checks:
+ # if we do not need to verify the checks then we can just
+ # run it straight through normally without using await.
+ return sorted(iterator, key=key) if sort else list(iterator)
+
+ # if we're here then we need to check every command if it can run
+ async def predicate(cmd):
+ try:
+ return await cmd.can_run(self.context)
+ except CommandError:
+ return False
+
+ ret = []
+ for cmd in iterator:
+ valid = await predicate(cmd)
+ if valid:
+ ret.append(cmd)
+
+ if sort:
+ ret.sort(key=key)
+ return ret
+
+ def get_max_size(self, commands):
+ """Returns the largest name length of the specified command list.
+
+ Parameters
+ ------------
+ commands: Sequence[:class:`Command`]
+ A sequence of commands to check for the largest size.
+
+ Returns
+ --------
+ :class:`int`
+ The maximum width of the commands.
+ """
+
+ as_lengths = (
+ discord.utils._string_width(c.name)
+ for c in commands
+ )
+ return max(as_lengths, default=0)
+
+ def get_destination(self):
+ """Returns the :class:`abc.Messageable` where the help command will be output.
+
+ You can override this method to customise the behaviour.
+
+ By default this returns the context's channel.
+ """
+ return self.context.channel
+
+ async def send_error_message(self, error):
+ """|coro|
+
+ Handles the implementation when an error happens in the help command.
+ For example, the result of :meth:`command_not_found` or
+ :meth:`command_has_no_subcommand_found` will be passed here.
+
+ You can override this method to customise the behaviour.
+
+ By default, this sends the error message to the destination
+ specified by :meth:`get_destination`.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ Parameters
+ ------------
+ error: :class:`str`
+ The error message to display to the user. Note that this has
+ had mentions removed to prevent abuse.
+ """
+ destination = self.get_destination()
+ await destination.send(error)
+
+ @_not_overriden
+ async def on_help_command_error(self, ctx, error):
+ """|coro|
+
+ The help command's error handler, as specified by :ref:`ext_commands_error_handler`.
+
+ Useful to override if you need some specific behaviour when the error handler
+ is called.
+
+ By default this method does nothing and just propagates to the default
+ error handlers.
+
+ Parameters
+ ------------
+ ctx: :class:`Context`
+ The invocation context.
+ error: :class:`CommandError`
+ The error that was raised.
+ """
+ pass
+
+ async def send_bot_help(self, mapping):
+ """|coro|
+
+ Handles the implementation of the bot command page in the help command.
+ This function is called when the help command is called with no arguments.
+
+ It should be noted that this method does not return anything -- rather the
+ actual message sending should be done inside this method. Well behaved subclasses
+ should use :meth:`get_destination` to know where to send, as this is a customisation
+ point for other users.
+
+ You can override this method to customise the behaviour.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ Also, the commands in the mapping are not filtered. To do the filtering
+ you will have to call :meth:`filter_commands` yourself.
+
+ Parameters
+ ------------
+ mapping: Mapping[Optional[:class:`Cog`], List[:class:`Command`]
+ A mapping of cogs to commands that have been requested by the user for help.
+ The key of the mapping is the :class:`~.commands.Cog` that the command belongs to, or
+ ``None`` if there isn't one, and the value is a list of commands that belongs to that cog.
+ """
+ return None
+
+ async def send_cog_help(self, cog):
+ """|coro|
+
+ Handles the implementation of the cog page in the help command.
+ This function is called when the help command is called with a cog as the argument.
+
+ It should be noted that this method does not return anything -- rather the
+ actual message sending should be done inside this method. Well behaved subclasses
+ should use :meth:`get_destination` to know where to send, as this is a customisation
+ point for other users.
+
+ You can override this method to customise the behaviour.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ To get the commands that belong to this cog see :meth:`Cog.get_commands`.
+ The commands returned not filtered. To do the filtering you will have to call
+ :meth:`filter_commands` yourself.
+
+ Parameters
+ -----------
+ cog: :class:`Cog`
+ The cog that was requested for help.
+ """
+ return None
+
+ async def send_group_help(self, group):
+ """|coro|
+
+ Handles the implementation of the group page in the help command.
+ This function is called when the help command is called with a group as the argument.
+
+ It should be noted that this method does not return anything -- rather the
+ actual message sending should be done inside this method. Well behaved subclasses
+ should use :meth:`get_destination` to know where to send, as this is a customisation
+ point for other users.
+
+ You can override this method to customise the behaviour.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ To get the commands that belong to this group without aliases see
+ :attr:`Group.commands`. The commands returned not filtered. To do the
+ filtering you will have to call :meth:`filter_commands` yourself.
+
+ Parameters
+ -----------
+ group: :class:`Group`
+ The group that was requested for help.
+ """
+ return None
+
+ async def send_command_help(self, command):
+ """|coro|
+
+ Handles the implementation of the single command page in the help command.
+
+ It should be noted that this method does not return anything -- rather the
+ actual message sending should be done inside this method. Well behaved subclasses
+ should use :meth:`get_destination` to know where to send, as this is a customisation
+ point for other users.
+
+ You can override this method to customise the behaviour.
+
+ .. note::
+
+ You can access the invocation context with :attr:`HelpCommand.context`.
+
+ .. admonition:: Showing Help
+ :class: helpful
+
+ There are certain attributes and methods that are helpful for a help command
+ to show such as the following:
+
+ - :attr:`Command.help`
+ - :attr:`Command.brief`
+ - :attr:`Command.short_doc`
+ - :attr:`Command.description`
+ - :meth:`get_command_signature`
+
+ There are more than just these attributes but feel free to play around with
+ these to help you get started to get the output that you want.
+
+ Parameters
+ -----------
+ command: :class:`Command`
+ The command that was requested for help.
+ """
+ return None
+
+ async def prepare_help_command(self, ctx, command=None):
+ """|coro|
+
+ A low level method that can be used to prepare the help command
+ before it does anything. For example, if you need to prepare
+ some state in your subclass before the command does its processing
+ then this would be the place to do it.
+
+ The default implementation is empty.
+
+ .. note::
+
+ This is called *inside* the help command callback body. So all
+ the usual rules that happen inside apply here as well.
+
+ Parameters
+ -----------
+ ctx: :class:`Context`
+ The invocation context.
+ command: Optional[:class:`str`]
+ The argument passed to the help command.
+ """
+ return None
+
+ async def command_callback(self, ctx, *, command=None):
+ """|coro|
+
+ The actual implementation of the help command.
+
+ It is not recommended to override this method and instead change
+ the behaviour through the methods that actually get dispatched.
+
+ - :meth:`send_bot_help`
+ - :meth:`send_cog_help`
+ - :meth:`send_group_help`
+ - :meth:`send_command_help`
+ - :meth:`get_destination`
+ - :meth:`command_not_found`
+ - :meth:`subcommand_not_found`
+ - :meth:`send_error_message`
+ - :meth:`on_help_command_error`
+ - :meth:`prepare_help_command`
+
+ """
+ self.context = ctx
+ await self.prepare_help_command(ctx, command)
+ bot = ctx.bot
+
+ if command is None:
+ mapping = {
+ cog: cog.get_commands()
+ for cog in bot.cogs.values()
+ }
+ mapping[None] = [c for c in bot.all_commands.values() if c.cog is None]
+ return await self.send_bot_help(mapping)
+
+ # Check if it's a cog
+ cog = bot.get_cog(command)
+ if cog is not None:
+ return await self.send_cog_help(cog)
+
+ maybe_coro = discord.utils.maybe_coroutine
+
+ # If it's not a cog then it's a command.
+ # Since we want to have detailed errors when someone
+ # passes an invalid subcommand, we need to walk through
+ # the command group chain ourselves.
+ keys = command.split(' ')
+ cmd = bot.all_commands.get(keys[0])
+ if cmd is None:
+ string = await maybe_coro(self.command_not_found, self.remove_mentions(keys[0]))
+ return await self.send_error_message(string)
+
+ for key in keys[1:]:
+ try:
+ found = cmd.all_commands.get(key)
+ except AttributeError:
+ string = await maybe_coro(self.subcommand_not_found, cmd, self.remove_mentions(key))
+ return await self.send_error_message(string)
+ else:
+ if found is None:
+ string = await maybe_coro(self.subcommand_not_found, cmd, self.remove_mentions(key))
+ return await self.send_error_message(string)
+ cmd = found
+
+ if isinstance(cmd, Group):
+ await self.send_group_help(cmd)
+ else:
+ await self.send_command_help(cmd)
+
+class DefaultHelpCommand(HelpCommand):
+ """The implementation of the default help command.
+
+ This derives from :class:`HelpCommand`.
+
+ It extends it with the following attributes.
+
+ Attributes
+ ------------
+ width: :class:`int`
+ The maximum number of characters that fit in a line.
+ Defaults to 80.
+ sort_commands: :class:`bool`
+ Whether to sort the commands in the output alphabetically. Defaults to ``True``.
+ indent: :class:`int`
+ How much to intend the commands from a heading. Defaults to ``2``.
+ commands_heading: :class:`str`
+ The command list's heading string used when the help command is invoked with a category name.
+ Useful for i18n. Defaults to ``"Commands:"``
+ no_category: :class:`str`
+ The string used when there is a command which does not belong to any category(cog).
+ Useful for i18n. Defaults to ``"No Category"``
+ paginator: :class:`Paginator`
+ The paginator used to paginate the help command output.
+ """
+
+ def __init__(self, **options):
+ self.width = options.pop('width', 80)
+ self.indent = options.pop('indent', 2)
+ self.sort_commands = options.pop('sort_commands', True)
+ self.commands_heading = options.pop('commands_heading', "Commands:")
+ self.no_category = options.pop('no_category', 'No Category')
+ self.paginator = options.pop('paginator', None)
+
+ if self.paginator is None:
+ self.paginator = Paginator()
+
+ super().__init__(**options)
+
+ def shorten_text(self, text):
+ """Shortens text to fit into the :attr:`width`."""
+ if len(text) > self.width:
+ return text[:self.width - 3] + '...'
+ return text
+
+ def get_ending_note(self):
+ """Returns help command's ending note. This is mainly useful to override for i18n purposes."""
+ command_name = self.context.invoked_with
+ return "Type {0}{1} command for more info on a command.\n" \
+ "You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name)
+
+ def add_indented_commands(self, commands, *, heading, max_size=None):
+ """Indents a list of commands after the specified heading.
+
+ The formatting is added to the :attr:`paginator`.
+
+ The default implementation is the command name indented by
+ :attr:`indent` spaces, padded to ``max_size`` followed by
+ the command's :attr:`Command.short_doc` and then shortened
+ to fit into the :attr:`width`.
+
+ Parameters
+ -----------
+ commands: Sequence[:class:`Command`]
+ A list of commands to indent for output.
+ heading: :class:`str`
+ The heading to add to the output. This is only added
+ if the list of commands is greater than 0.
+ max_size: Optional[:class:`int`]
+ The max size to use for the gap between indents.
+ If unspecified, calls :meth:`get_max_size` on the
+ commands parameter.
+ """
+
+ if not commands:
+ return
+
+ self.paginator.add_line(heading)
+ max_size = max_size or self.get_max_size(commands)
+
+ get_width = discord.utils._string_width
+ for command in commands:
+ name = command.name
+ width = max_size - (get_width(name) - len(name))
+ entry = '{0}{1:<{width}} {2}'.format(self.indent * ' ', name, command.short_doc, width=width)
+ self.paginator.add_line(self.shorten_text(entry))
+
+ async def send_pages(self):
+ """A helper utility to send the page output from :attr:`paginator` to the destination."""
+ destination = self.get_destination()
+ for page in self.paginator.pages:
+ await destination.send(page)
+
+ def add_command_formatting(self, command):
+ """A utility function to format the non-indented block of commands and groups.
+
+ Parameters
+ ------------
+ command: :class:`Command`
+ The command to format.
+ """
+
+ if command.description:
+ self.paginator.add_line(command.description, empty=True)
+
+ signature = self.get_command_signature(command)
+ self.paginator.add_line(signature, empty=True)
+
+ if command.help:
+ self.paginator.add_line(command.help, empty=True)
+
+ async def prepare_help_command(self, ctx, command):
+ self.paginator.clear()
+
+ async def send_bot_help(self, mapping):
+ ctx = self.context
+ bot = ctx.bot
+
+ if bot.description:
+ # <description> portion
+ self.paginator.add_line(bot.description, empty=True)
+
+ no_category = '\u200b{0.no_category}:'.format(self)
+ def get_category(command, *, no_category=no_category):
+ cog = command.cog
+ return cog.qualified_name + ':' if cog is not None else no_category
+
+ filtered = await self.filter_commands(bot.commands, sort=True, key=get_category)
+ max_size = self.get_max_size(filtered)
+ to_iterate = itertools.groupby(filtered, key=get_category)
+
+ # Now we can add the commands to the page.
+ for category, commands in to_iterate:
+ commands = sorted(commands, key=lambda c: c.name) if self.sort_commands else list(commands)
+ self.add_indented_commands(commands, heading=category, max_size=max_size)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_command_help(self, command):
+ self.add_command_formatting(command)
+ self.paginator.close_page()
+ await self.send_pages()
+
+ async def send_group_help(self, group):
+ self.add_command_formatting(group)
+
+ filtered = await self.filter_commands(group.commands, sort=self.sort_commands)
+ self.add_indented_commands(filtered, heading=self.commands_heading)
+
+ if filtered:
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_cog_help(self, cog):
+ if cog.description:
+ self.paginator.add_line(cog.description, empty=True)
+
+ filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
+ self.add_indented_commands(filtered, heading=self.commands_heading)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+class MinimalHelpCommand(HelpCommand):
+ """An implementation of a help command with minimal output.
+
+ This derives from :class:`HelpCommand`.
+
+ Attributes
+ ------------
+ sort_commands: :class:`bool`
+ Whether to sort the commands in the output alphabetically. Defaults to ``True``.
+ commands_heading: :class:`str`
+ The command list's heading string used when the help command is invoked with a category name.
+ Useful for i18n. Defaults to ``"Commands"``
+ aliases_heading: :class:`str`
+ The alias list's heading string used to list the aliases of the command. Useful for i18n.
+ Defaults to ``"Aliases:"``.
+ no_category: :class:`str`
+ The string used when there is a command which does not belong to any category(cog).
+ Useful for i18n. Defaults to ``"No Category"``
+ paginator: :class:`Paginator`
+ The paginator used to paginate the help command output.
+ """
+
+ def __init__(self, **options):
+ self.sort_commands = options.pop('sort_commands', True)
+ self.commands_heading = options.pop('commands_heading', "Commands")
+ self.aliases_heading = options.pop('aliases_heading', "Aliases:")
+ self.no_category = options.pop('no_category', 'No Category')
+ self.paginator = options.pop('paginator', None)
+
+ if self.paginator is None:
+ self.paginator = Paginator(suffix=None, prefix=None)
+
+ super().__init__(**options)
+
+ async def send_pages(self):
+ """A helper utility to send the page output from :attr:`paginator` to the destination."""
+ destination = self.get_destination()
+ for page in self.paginator.pages:
+ await destination.send(page)
+
+ def get_opening_note(self):
+ """Returns help command's opening note. This is mainly useful to override for i18n purposes.
+
+ The default implementation returns ::
+
+ Use `{prefix}{command_name} <command>` for more info on a command.
+ You can also use `{prefix}{command_name} <category>` for more info on a category.
+
+ """
+ command_name = self.context.invoked_with
+ return "Use `{0}{1} <command>` for more info on a command.\n" \
+ "You can also use `{0}{1} <category>` for more info on a category.".format(self.clean_prefix, command_name)
+
+ def get_command_signature(self, command):
+ return '%s%s %s' % (self.clean_prefix, command.qualified_name, command.signature)
+
+ def get_ending_note(self):
+ """Return the help command's ending note. This is mainly useful to override for i18n purposes.
+
+ The default implementation does nothing.
+ """
+ return None
+
+ def add_bot_commands_formatting(self, commands, heading):
+ """Adds the minified bot heading with commands to the output.
+
+ The formatting should be added to the :attr:`paginator`.
+
+ The default implementation is a bold underline heading followed
+ by commands separated by an EN SPACE (U+2002) in the next line.
+
+ Parameters
+ -----------
+ commands: Sequence[:class:`Command`]
+ A list of commands that belong to the heading.
+ heading: :class:`str`
+ The heading to add to the line.
+ """
+ if commands:
+ # U+2002 Middle Dot
+ joined = '\u2002'.join(c.name for c in commands)
+ self.paginator.add_line('__**%s**__' % heading)
+ self.paginator.add_line(joined)
+
+ def add_subcommand_formatting(self, command):
+ """Adds formatting information on a subcommand.
+
+ The formatting should be added to the :attr:`paginator`.
+
+ The default implementation is the prefix and the :attr:`Command.qualified_name`
+ optionally followed by an En dash and the command's :attr:`Command.short_doc`.
+
+ Parameters
+ -----------
+ command: :class:`Command`
+ The command to show information of.
+ """
+ fmt = '{0}{1} \N{EN DASH} {2}' if command.short_doc else '{0}{1}'
+ self.paginator.add_line(fmt.format(self.clean_prefix, command.qualified_name, command.short_doc))
+
+ def add_aliases_formatting(self, aliases):
+ """Adds the formatting information on a command's aliases.
+
+ The formatting should be added to the :attr:`paginator`.
+
+ The default implementation is the :attr:`aliases_heading` bolded
+ followed by a comma separated list of aliases.
+
+ This is not called if there are no aliases to format.
+
+ Parameters
+ -----------
+ aliases: Sequence[:class:`str`]
+ A list of aliases to format.
+ """
+ self.paginator.add_line('**%s** %s' % (self.aliases_heading, ', '.join(aliases)), empty=True)
+
+ def add_command_formatting(self, command):
+ """A utility function to format commands and groups.
+
+ Parameters
+ ------------
+ command: :class:`Command`
+ The command to format.
+ """
+
+ if command.description:
+ self.paginator.add_line(command.description, empty=True)
+
+ signature = self.get_command_signature(command)
+ if command.aliases:
+ self.paginator.add_line(signature)
+ self.add_aliases_formatting(command.aliases)
+ else:
+ self.paginator.add_line(signature, empty=True)
+
+ if command.help:
+ self.paginator.add_line(command.help, empty=True)
+
+ async def prepare_help_command(self, ctx, command):
+ self.paginator.clear()
+
+ async def send_bot_help(self, mapping):
+ ctx = self.context
+ bot = ctx.bot
+
+ if bot.description:
+ self.paginator.add_line(bot.description, empty=True)
+
+ note = self.get_opening_note()
+ if note:
+ self.paginator.add_line(note, empty=True)
+
+ no_category = '\u200b{0.no_category}'.format(self)
+ def get_category(command, *, no_category=no_category):
+ cog = command.cog
+ return cog.qualified_name if cog is not None else no_category
+
+ filtered = await self.filter_commands(bot.commands, sort=True, key=get_category)
+ to_iterate = itertools.groupby(filtered, key=get_category)
+
+ for category, commands in to_iterate:
+ commands = sorted(commands, key=lambda c: c.name) if self.sort_commands else list(commands)
+ self.add_bot_commands_formatting(commands, category)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_cog_help(self, cog):
+ bot = self.context.bot
+ if bot.description:
+ self.paginator.add_line(bot.description, empty=True)
+
+ note = self.get_opening_note()
+ if note:
+ self.paginator.add_line(note, empty=True)
+
+ filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
+ if filtered:
+ self.paginator.add_line('**%s %s**' % (cog.qualified_name, self.commands_heading))
+ for command in filtered:
+ self.add_subcommand_formatting(command)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_group_help(self, group):
+ self.add_command_formatting(group)
+
+ filtered = await self.filter_commands(group.commands, sort=self.sort_commands)
+ if filtered:
+ note = self.get_opening_note()
+ if note:
+ self.paginator.add_line(note, empty=True)
+
+ self.paginator.add_line('**%s**' % self.commands_heading)
+ for command in filtered:
+ self.add_subcommand_formatting(command)
+
+ note = self.get_ending_note()
+ if note:
+ self.paginator.add_line()
+ self.paginator.add_line(note)
+
+ await self.send_pages()
+
+ async def send_command_help(self, command):
+ self.add_command_formatting(command)
+ self.paginator.close_page()
+ await self.send_pages()