diff options
Diffstat (limited to 'discord')
| -rw-r--r-- | discord/ext/commands/__init__.py | 2 | ||||
| -rw-r--r-- | discord/ext/commands/bot.py | 147 | ||||
| -rw-r--r-- | discord/ext/commands/core.py | 20 | ||||
| -rw-r--r-- | discord/ext/commands/formatter.py | 352 | ||||
| -rw-r--r-- | discord/ext/commands/help.py | 1144 |
5 files changed, 1189 insertions, 476 deletions
diff --git a/discord/ext/commands/__init__.py b/discord/ext/commands/__init__.py index b8195c62..b14fd655 100644 --- a/discord/ext/commands/__init__.py +++ b/discord/ext/commands/__init__.py @@ -14,7 +14,7 @@ from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or from .context import Context from .core import * from .errors import * -from .formatter import HelpFormatter, Paginator +from .help import * from .converter import * from .cooldowns import * from .cog import * diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 694b466d..efa375d5 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -38,7 +38,7 @@ from .core import GroupMixin, Command from .view import StringView from .context import Context from .errors import CommandNotFound, CommandError -from .formatter import HelpFormatter +from .help import HelpCommand, DefaultHelpCommand from .cog import Cog def when_mentioned(bot, msg): @@ -84,71 +84,17 @@ def when_mentioned_or(*prefixes): return inner -_mentions_transforms = { - '@everyone': '@\u200beveryone', - '@here': '@\u200bhere' -} - -_mention_pattern = re.compile('|'.join(_mentions_transforms.keys())) - def _is_submodule(parent, child): return parent == child or child.startswith(parent + ".") -async def _default_help_command(ctx, *commands : str): - """Shows this message.""" - bot = ctx.bot - destination = ctx.message.author if bot.pm_help else ctx.message.channel - - def repl(obj): - return _mentions_transforms.get(obj.group(0), '') - - # help by itself just lists our own commands. - if len(commands) == 0: - pages = await bot.formatter.format_help_for(ctx, bot) - elif len(commands) == 1: - # try to see if it is a cog name - name = _mention_pattern.sub(repl, commands[0]) - command = None - if name in bot.cogs: - command = bot.cogs[name] - else: - command = bot.all_commands.get(name) - if command is None: - await destination.send(bot.command_not_found.format(name)) - return - - pages = await bot.formatter.format_help_for(ctx, command) - else: - name = _mention_pattern.sub(repl, commands[0]) - command = bot.all_commands.get(name) - if command is None: - await destination.send(bot.command_not_found.format(name)) - return - - for key in commands[1:]: - try: - key = _mention_pattern.sub(repl, key) - command = command.all_commands.get(key) - if command is None: - await destination.send(bot.command_not_found.format(key)) - return - except AttributeError: - await destination.send(bot.command_has_no_subcommands.format(command, key)) - return - - pages = await bot.formatter.format_help_for(ctx, command) +class _DefaultRepr: + def __repr__(self): + return '<default-help-command>' - if bot.pm_help is None: - characters = sum(map(len, pages)) - # modify destination based on length of pages. - if characters > 1000: - destination = ctx.message.author - - for page in pages: - await destination.send(page) +_default = _DefaultRepr() class BotBase(GroupMixin): - def __init__(self, command_prefix, formatter=None, description=None, pm_help=False, **options): + def __init__(self, command_prefix, help_command=_default, description=None, **options): super().__init__(**options) self.command_prefix = command_prefix self.extra_events = {} @@ -158,32 +104,19 @@ class BotBase(GroupMixin): self._check_once = [] self._before_invoke = None self._after_invoke = None + self._help_command = None self.description = inspect.cleandoc(description) if description else '' - self.pm_help = pm_help self.owner_id = options.get('owner_id') - self.command_not_found = options.pop('command_not_found', 'No command called "{}" found.') - self.command_has_no_subcommands = options.pop('command_has_no_subcommands', 'Command {0.name} has no subcommands.') if options.pop('self_bot', False): self._skip_check = lambda x, y: x != y else: self._skip_check = lambda x, y: x == y - self.help_attrs = options.pop('help_attrs', {}) - - if 'name' not in self.help_attrs: - self.help_attrs['name'] = 'help' - - if formatter is not None: - if not isinstance(formatter, HelpFormatter): - raise discord.ClientException('Formatter must be a subclass of HelpFormatter') - self.formatter = formatter + if help_command is _default: + self.help_command = DefaultHelpCommand() else: - self.formatter = HelpFormatter() - - # pay no mind to this ugliness. - help_cmd = Command(_default_help_command, **self.help_attrs) - self.add_command(help_cmd) + self.help_command = help_command # internal helpers @@ -577,6 +510,9 @@ class BotBase(GroupMixin): if cog is None: return + help_command = self._help_command + if help_command and help_command.cog is cog: + help_command.cog = None cog._eject(self) # extensions @@ -685,6 +621,27 @@ class BotBase(GroupMixin): if _is_submodule(lib_name, module): del sys.modules[module] + # help command stuff + + @property + def help_command(self): + return self._help_command + + @help_command.setter + def help_command(self, value): + if value is not None: + if not isinstance(value, HelpCommand): + raise discord.ClientException('help_command must be a subclass of HelpCommand') + if self._help_command is not None: + self._help_command._remove_from_bot(self) + self._help_command = value + value._add_to_bot(self) + elif self._help_command is not None: + self._help_command._remove_from_bot(self) + self._help_command = None + else: + self._help_command = None + # command processing async def get_prefix(self, message): @@ -899,40 +856,16 @@ class Bot(BotBase, discord.Client): Whether the commands should be case insensitive. Defaults to ``False``. This attribute does not carry over to groups. You must set it to every group if you require group commands to be case insensitive as well. - description : :class:`str` + description: :class:`str` The content prefixed into the default help message. - self_bot : :class:`bool` + self_bot: :class:`bool` If ``True``, the bot will only listen to commands invoked by itself rather than ignoring itself. If ``False`` (the default) then the bot will ignore itself. This cannot be changed once initialised. - formatter : :class:`.HelpFormatter` - The formatter used to format the help message. By default, it uses - the :class:`.HelpFormatter`. Check it for more info on how to override it. - If you want to change the help command completely (add aliases, etc) then - a call to :meth:`~.Bot.remove_command` with 'help' as the argument would do the - trick. - pm_help : Optional[:class:`bool`] - A tribool that indicates if the help command should PM the user instead of - sending it to the channel it received it from. If the boolean is set to - ``True``, then all help output is PM'd. If ``False``, none of the help - output is PM'd. If ``None``, then the bot will only PM when the help - message becomes too long (dictated by more than 1000 characters). - Defaults to ``False``. - help_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. Note that ``pass_context`` - will always be set to ``True`` regardless of what you pass in. - command_not_found : :class:`str` - The format string used when the help command is invoked with a command that - is not found. Useful for i18n. Defaults to ``"No command called {} found."``. - The only format argument is the name of the command passed. - command_has_no_subcommands : :class:`str` - The format string used when the help command is invoked with requests for a - subcommand but the command does not have any subcommands. Defaults to - ``"Command {0.name} has no subcommands."``. The first format argument is the - :class:`.Command` attempted to get a subcommand and the second is the name. + help_command: Optional[:class:`.HelpCommand`] + The help command implementation to use. This can be dynamically + set at runtime. To remove the help command pass ``None``. For more + information on implementing a help command, see :ref:`ext_commands_help_command`. owner_id: Optional[:class:`int`] The ID that owns the bot. If this is not set and is then queried via :meth:`.is_owner` then it is fetched automatically using diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 72f25a69..f928e7f3 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -810,27 +810,15 @@ class Command(_BaseCommand): @property def signature(self): """Returns a POSIX-like signature useful for help command output.""" - result = [] - parent = self.full_parent_name - - if len(self.aliases) > 0: - aliases = '|'.join(self.aliases) - fmt = '[%s|%s]' % (self.name, aliases) - if parent: - fmt = parent + ' ' + fmt - result.append(fmt) - else: - name = self.name if not parent else parent + ' ' + self.name - result.append(name) - if self.usage is not None: - result.append(self.usage) - return ' '.join(result) + return self.usage + params = self.clean_params if not params: - return ' '.join(result) + return '' + result = [] for name, param in params.items(): greedy = isinstance(param.annotation, converters._Greedy) diff --git a/discord/ext/commands/formatter.py b/discord/ext/commands/formatter.py deleted file mode 100644 index d9d61576..00000000 --- a/discord/ext/commands/formatter.py +++ /dev/null @@ -1,352 +0,0 @@ -# -*- 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 inspect -import discord.utils - -from .core import GroupMixin, Command -from .errors import CommandError -# from discord.iterators import _FilteredAsyncIterator - -# 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: :class:`str` - The prefix inserted to every page. e.g. three backticks. - suffix: :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 - len(suffix) - self._current_page = [prefix] - self._count = len(prefix) + 1 # prefix + newline - self._pages = [] - - 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`. - """ - if len(line) > self.max_size - len(self.prefix) - 2: - raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2)) - - 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.""" - self._current_page.append(self.suffix) - self._pages.append('\n'.join(self._current_page)) - self._current_page = [self.prefix] - self._count = len(self.prefix) + 1 # prefix + newline - - @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) - -class HelpFormatter: - """The default base implementation that handles formatting of the help - command. - - To override the behaviour of the formatter, :meth:`~.HelpFormatter.format` - should be overridden. A number of utility functions are provided for use - inside that method. - - Attributes - ----------- - show_hidden: :class:`bool` - Dictates if hidden commands should be shown in the output. - Defaults to ``False``. - show_check_failure: :class:`bool` - Dictates if commands that have their :attr:`.Command.checks` failed - shown. Defaults to ``False``. - width: :class:`int` - The maximum number of characters that fit in a line. - Defaults to 80. - 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"`` - """ - def __init__(self, show_hidden=False, show_check_failure=False, width=80, - commands_heading="Commands:", no_category="No Category"): - self.width = width - self.show_hidden = show_hidden - self.show_check_failure = show_check_failure - self.commands_heading = commands_heading - self.no_category = no_category - - def has_subcommands(self): - """:class:`bool`: Specifies if the command has subcommands.""" - return isinstance(self.command, GroupMixin) - - def is_bot(self): - """:class:`bool`: Specifies if the command being formatted is the bot itself.""" - return self.command is self.context.bot - - def is_cog(self): - """:class:`bool`: Specifies if the command being formatted is actually a cog.""" - return not self.is_bot() and not isinstance(self.command, Command) - - def shorten(self, text): - """Shortens text to fit into the :attr:`width`.""" - if len(text) > self.width: - return text[:self.width - 3] + '...' - return text - - @property - def max_name_size(self): - """:class:`int`: Returns the largest name length of a command or if it has subcommands - the largest subcommand name.""" - try: - commands = self.command.all_commands if not self.is_cog() else self.context.bot.all_commands - if commands: - return max(map(lambda c: discord.utils._string_width(c.name) if self.show_hidden or not c.hidden else 0, commands.values())) - return 0 - except AttributeError: - return len(self.command.name) - - @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): - """Retrieves the signature portion of the help page.""" - prefix = self.clean_prefix - cmd = self.command - return prefix + cmd.signature - - 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) - - async def filter_command_list(self): - """Returns a filtered list of commands based on the two attributes - provided, :attr:`show_check_failure` and :attr:`show_hidden`. - Also filters based on if :meth:`~.HelpFormatter.is_cog` is valid. - - Returns - -------- - iterable - An iterable with the filter being applied. The resulting value is - a (key, value) :class:`tuple` of the command name and the command itself. - """ - - def sane_no_suspension_point_predicate(tup): - cmd = tup[1] - if self.is_cog(): - # filter commands that don't exist to this cog. - if cmd.cog is not self.command: - return False - - if cmd.hidden and not self.show_hidden: - return False - - return True - - async def predicate(tup): - if sane_no_suspension_point_predicate(tup) is False: - return False - - cmd = tup[1] - try: - return await cmd.can_run(self.context) - except CommandError: - return False - - iterator = self.command.all_commands.items() if not self.is_cog() else self.context.bot.all_commands.items() - if self.show_check_failure: - return filter(sane_no_suspension_point_predicate, iterator) - - # Gotta run every check and verify it - ret = [] - for elem in iterator: - valid = await predicate(elem) - if valid: - ret.append(elem) - - return ret - - def _add_subcommands_to_page(self, max_width, commands): - for name, command in commands: - if name in command.aliases: - # skip aliases - continue - width_gap = discord.utils._string_width(name) - len(name) - entry = ' {0:<{width}} {1}'.format(name, command.short_doc, width=max_width-width_gap) - shortened = self.shorten(entry) - self._paginator.add_line(shortened) - - async def format_help_for(self, context, command_or_bot): - """Formats the help page and handles the actual heavy lifting of how - the help command looks like. To change the behaviour, override the - :meth:`~.HelpFormatter.format` method. - - Parameters - ----------- - context: :class:`.Context` - The context of the invoked help command. - command_or_bot: :class:`.Command` or :class:`.Bot` - The bot or command that we are getting the help of. - - Returns - -------- - list - A paginated output of the help command. - """ - self.context = context - self.command = command_or_bot - return await self.format() - - async def format(self): - """Handles the actual behaviour involved with formatting. - - To change the behaviour, this method should be overridden. - - Returns - -------- - list - A paginated output of the help command. - """ - self._paginator = Paginator() - - # we need a padding of ~80 or so - - description = self.command.description if not self.is_cog() else inspect.getdoc(self.command) - - if description: - # <description> portion - self._paginator.add_line(description, empty=True) - - if isinstance(self.command, Command): - # <signature portion> - signature = self.get_command_signature() - self._paginator.add_line(signature, empty=True) - - # <long doc> section - if self.command.help: - self._paginator.add_line(self.command.help, empty=True) - - # end it here if it's just a regular command - if not self.has_subcommands(): - self._paginator.close_page() - return self._paginator.pages - - max_width = self.max_name_size - - def category(tup): - cog = tup[1].cog_name - # we insert the zero width space there to give it approximate - # last place sorting position. - return cog + ':' if cog is not None else '\u200b' + self.no_category + ':' - - filtered = await self.filter_command_list() - if self.is_bot(): - data = sorted(filtered, key=category) - for category, commands in itertools.groupby(data, key=category): - # there simply is no prettier way of doing this. - commands = sorted(commands) - if len(commands) > 0: - self._paginator.add_line(category) - - self._add_subcommands_to_page(max_width, commands) - else: - filtered = sorted(filtered) - if filtered: - self._paginator.add_line(self.commands_heading) - self._add_subcommands_to_page(max_width, filtered) - - # add the ending note - self._paginator.add_line() - ending_note = self.get_ending_note() - self._paginator.add_line(ending_note) - return self._paginator.pages 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() |