diff options
| author | 2018-07-26 22:49:04 +0000 | |
|---|---|---|
| committer | 2018-07-26 22:49:04 +0000 | |
| commit | 914e40104ac2b4eb4760bc421578b9a39ca47b10 (patch) | |
| tree | ed19a92a2a90455e9378e87e405ac3bb9daed636 | |
| parent | Linting (diff) | |
Remove python syntax parser
| -rw-r--r-- | bot/__init__.py | 218 | ||||
| -rw-r--r-- | bot/__main__.py | 14 | ||||
| -rw-r--r-- | bot/cogs/bigbrother.py | 12 | ||||
| -rw-r--r-- | bot/cogs/bot.py | 25 | ||||
| -rw-r--r-- | bot/cogs/cogs.py | 20 | ||||
| -rw-r--r-- | bot/cogs/defcon.py | 24 | ||||
| -rw-r--r-- | bot/cogs/deployment.py | 18 | ||||
| -rw-r--r-- | bot/cogs/doc.py | 30 | ||||
| -rw-r--r-- | bot/cogs/eval.py | 8 | ||||
| -rw-r--r-- | bot/cogs/events.py | 4 | ||||
| -rw-r--r-- | bot/cogs/hiphopify.py | 6 | ||||
| -rw-r--r-- | bot/cogs/off_topic_names.py | 14 | ||||
| -rw-r--r-- | bot/cogs/snakes.py | 80 | ||||
| -rw-r--r-- | bot/cogs/snekbox.py | 9 | ||||
| -rw-r--r-- | bot/cogs/tags.py | 48 | ||||
| -rw-r--r-- | bot/cogs/utils.py | 4 | ||||
| -rw-r--r-- | bot/cogs/verification.py | 24 | ||||
| -rw-r--r-- | bot/formatter.py | 152 | 
18 files changed, 165 insertions, 545 deletions
| diff --git a/bot/__init__.py b/bot/__init__.py index d446897b1..a87d31541 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -1,11 +1,8 @@ -import ast  import logging  import os -import re  import sys  from logging import Logger, StreamHandler, handlers -import discord.ext.commands.view  from logmatic import JsonFormatter  logging.TRACE = 5 @@ -97,218 +94,3 @@ logging.getLogger("discord.gateway").setLevel(logging.ERROR)  logging.getLogger("discord.state").setLevel(logging.ERROR)  logging.getLogger("discord.http").setLevel(logging.ERROR)  logging.getLogger("websockets.protocol").setLevel(logging.ERROR) - - -def _skip_string(self, string: str) -> bool: -    """ -    Our version of the skip_string method from -    discord.ext.commands.view; used to find -    the prefix in a message, but allowing prefix -    to ignore case sensitivity -    """ - -    strlen = len(string) -    if self.buffer.lower()[self.index:self.index + strlen] == string: -        self.previous = self.index -        self.index += strlen -        return True -    return False - - -def _get_word(self) -> str: -    """ -    Invokes the get_word method from -    discord.ext.commands.view used to find -    the bot command part of a message, but -    allows the command to ignore case sensitivity, -    and allows commands to have Python syntax. -    """ - -    def parse_python(buffer_pos): -        """ -        Takes the instance of the view and parses the buffer, if it contains valid python syntax. -        This may fail spectacularly with a SyntaxError, which must be caught by the caller. - -        Example of valid Python syntax calls: -        ------------------------------ -        bot.tags.set("test", 'a dark, dark night') -        bot.help(tags.delete) -        bot.hELP(tags.delete) -        bot.tags['internet'] -        bot.tags['internet'] = "A series of tubes" - -        :return: the parsed command -        """ - -        # Check what's after the '(' or '[' -        next_char = None -        if len(self.buffer) - 1 != self.index: -            next_char = self.buffer[self.index + 1] - -        # Catch raw channel, member or role mentions and wrap them in quotes. -        tempbuffer = self.buffer -        tempbuffer = re.sub(r"(<(?:@|@!|[#&])\d+>)", -                            r'"\1"', -                            tempbuffer) - -        # Let's parse! -        log.debug("A python-style command was used. Attempting to parse. " -                  f"Buffer is '{self.buffer}'. Tempbuffer is '{tempbuffer}'. " -                  "A step-by-step can be found in the trace log.") - -        if current == "(" and next_char == ")": -            # Move the cursor to capture the ()'s -            log.debug("User called command without providing arguments.") -            buffer_pos += 2 -            parsed_result = self.buffer[self.previous:self.index + (buffer_pos+2)] -            self.index += 2 -            return parsed_result - -        elif current == "(" and next_char: - -            # Parse the args -            log.trace(f"Parsing command with ast.literal_eval. args are {tempbuffer[self.index:]}") -            args = tempbuffer[self.index:] -            args = ast.literal_eval(args) - -            # Return what we'd return for a non-python syntax call -            log.trace(f"Returning {self.buffer[self.previous:self.index]}") -            parsed_result = self.buffer[self.previous:self.index] - -        elif current == "(" or current == "[" and not next_char: - -            # Just remove the start bracket -            log.debug("User called command with a single bracket. Removing bracket.") -            parsed_result = self.buffer[self.previous:self.index] -            args = None - -        # Check if a command in the form of `bot.tags['ask']` -        # or alternatively `bot.tags['ask'] = 'whatever'` was used. -        elif current == "[": - -            # Syntax is `bot.tags['ask']` => mimic `getattr` -            log.trace(f"Got a command candidate for getitem / setitem parsing: {self.buffer}") -            if self.buffer.endswith("]"): - -                # Key: The first argument, specified `bot.tags[here]` -                key = tempbuffer[self.index + 1:tempbuffer.rfind("]")] -                log.trace(f"Command mimicks getitem. Key: {key!r}") -                args = ast.literal_eval(key) - -                # Use the cogs `.get` method. -                parsed_result = self.buffer[self.previous:self.index] + ".get" - -            # Syntax is `bot.tags['ask'] = 'whatever'` => mimic `setattr` -            elif "=" in self.buffer and not self.buffer.endswith("="): -                equals_pos = tempbuffer.find("=") -                closing_bracket_pos = tempbuffer.rfind("]", 0, equals_pos) - -                # Key: The first argument, specified `bot.tags[here]` -                key_contents = tempbuffer[self.index + 1:closing_bracket_pos] -                key = ast.literal_eval(key_contents) - -                # Value: The second argument, specified after the `=` -                right_hand = tempbuffer.split("=", maxsplit=1)[1].strip() -                value = ast.literal_eval(right_hand) - -                # If the value is a falsy value - mimick `bot.tags.delete(key)` -                if not value: -                    log.trace(f"Command mimicks delitem. Key: {key!r}.") -                    parsed_result = self.buffer[self.previous:self.index] + ".delete" -                    args = key - -                # Otherwise, assume assignment, for example `bot.tags['this'] = 'that'` -                else: -                    log.trace(f"Command mimicks setitem. Key: {key!r}, value: {value!r}.") -                    parsed_result = self.buffer[self.previous:self.index] + ".set" -                    args = (key, value) - -            # Syntax is god knows what, pass it along -            else: -                parsed_result = self.buffer -                args = '' -                log.trace(f"Command is of unknown syntax: {self.buffer}") - -        # Args handling -        new_args = [] - -        if args: -            # Force args into container -            if not isinstance(args, tuple): -                args = (args,) - -            # Type validate and format -            for arg in args: - -                # Other types get converted to strings -                if not isinstance(arg, str): -                    log.trace(f"{arg} is not a str, casting to str.") -                    arg = str(arg) - -                # Allow using double quotes within triple double quotes -                arg = arg.replace('"', '\\"') - -                # Adding double quotes to every argument -                log.trace("Wrapping all args in double quotes.") -                new_args.append(f'"{arg}"') - -        # Reconstruct valid discord.py syntax -        prefix = self.buffer[:self.previous] -        self.buffer = f"{prefix}{parsed_result}" - -        if new_args: -            self.buffer += (" " + " ".join(new_args)) - -        self.index = len(f"{prefix}{parsed_result}") -        self.end = len(self.buffer) -        log.trace(f"Modified the buffer. New buffer is now '{self.buffer}'") - -        return parsed_result - -    # Iterate through the buffer and determine -    pos = 0 -    current = None -    while not self.eof: -        try: -            current = self.buffer[self.index + pos] -            if current.isspace() or current == "(" or current == "[": -                break -            pos += 1 -        except IndexError: -            break - -    self.previous = self.index -    result = self.buffer[self.index:self.index + pos] -    self.index += pos - -    # If the command looks like a python syntax command, try to parse it. -    if current == "(" or current == "[": -        try: -            result = parse_python(pos) - -        except SyntaxError: -            log.debug( -                "A SyntaxError was encountered while parsing a python-syntaxed command:" -                "\nTraceback (most recent call last):\n" -                '  File "<stdin>", line 1, in <module>\n' -                f"    {self.buffer}\n" -                f"     {' ' * self.index}^\n" -                "SyntaxError: invalid syntax" -            ) -            return - -        except ValueError: -            log.debug( -                "A ValueError was encountered while parsing a python-syntaxed command:" -                "\nTraceback (most recent call last):\n" -                '  File "<stdin>", line 1, in <module>\n' -                f"ValueError: could not ast.literal_eval the following: '{self.buffer}'" -            ) -            return - -    return result - - -# Monkey patch the methods -discord.ext.commands.view.StringView.skip_string = _skip_string -discord.ext.commands.view.StringView.get_word = _get_word diff --git a/bot/__main__.py b/bot/__main__.py index b688de33e..4429c2a0d 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -6,24 +6,14 @@ from discord import Game  from discord.ext.commands import Bot, when_mentioned_or  from bot.constants import Bot as BotConfig  # , ClickUp -from bot.formatter import Formatter  from bot.utils.service_discovery import wait_for_rmq  log = logging.getLogger(__name__)  bot = Bot( -    command_prefix=when_mentioned_or( -        "self.", "bot." -    ), -    activity=Game( -        name="Commands: bot.help()" -    ), -    help_attrs={ -        "name": "help()", -        "aliases": ["help"] -    }, -    formatter=Formatter(), +    command_prefix=when_mentioned_or("!"), +    activity=Game(name="Commands: !help"),      case_insensitive=True,      max_messages=10_000  ) diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py index 4d0996122..523e85f1d 100644 --- a/bot/cogs/bigbrother.py +++ b/bot/cogs/bigbrother.py @@ -2,7 +2,7 @@ import logging  from typing import List, Union  from discord import Color, Embed, Guild, Member, Message, TextChannel, User -from discord.ext.commands import Bot, Context, command +from discord.ext.commands import Bot, Context, group  from bot.constants import Channels, Emojis, Guild as GuildConfig, Keys, Roles, URLs  from bot.decorators import with_role @@ -79,7 +79,11 @@ class BigBrother:              await channel.send(relay_content) -    @command(name='bigbrother.watched()', aliases=('bigbrother.watched',)) +    @group(name='bigbrother', aliases=('bb',)) +    async def bigbrother_group(self, ctx: Context): +        """Monitor users, NSA-style.""" + +    @bigbrother_group.command(name='watched', aliases=('all',))      @with_role(Roles.owner, Roles.admin, Roles.moderator)      async def watched_command(self, ctx: Context, from_cache: bool = True):          """ @@ -117,7 +121,7 @@ class BigBrother:                  else:                      await ctx.send(f":x: got non-200 response from the API") -    @command(name='bigbrother.watch()', aliases=('bigbrother.watch',)) +    @bigbrother_group.command(name='watch', aliases=('w',))      @with_role(Roles.owner, Roles.admin, Roles.moderator)      async def watch_command(self, ctx: Context, user: User, channel: TextChannel = None):          """ @@ -156,7 +160,7 @@ class BigBrother:                  reason = data.get('error_message', "no message provided")                  await ctx.send(f":x: the API returned an error: {reason}") -    @command(name='bigbrother.unwatch()', aliases=('bigbrother.unwatch',)) +    @bigbrother_group.command(name='unwatch', aliases=('uw',))      @with_role(Roles.owner, Roles.admin, Roles.moderator)      async def unwatch_command(self, ctx: Context, user: User):          """Stop relaying messages by the given `user`.""" diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py index e79fc7ada..2f8600c06 100644 --- a/bot/cogs/bot.py +++ b/bot/cogs/bot.py @@ -49,15 +49,15 @@ class Bot:          await ctx.invoke(self.bot.get_command("help"), "bot") -    @bot_group.command(aliases=["about"], hidden=True) +    @bot_group.command(name='about', aliases=('info',), hidden=True)      @with_role(Roles.verified) -    async def info(self, ctx: Context): +    async def about_command(self, ctx: Context):          """          Get information about the bot          """          embed = Embed( -            description="A utility bot designed just for the Python server! Try `bot.help()` for more info.", +            description="A utility bot designed just for the Python server! Try `!help()` for more info.",              url="https://gitlab.com/discord-python/projects/bot"          ) @@ -73,30 +73,21 @@ class Bot:              icon_url=URLs.bot_avatar          ) -        log.info(f"{ctx.author} called bot.about(). Returning information about the bot.") +        log.info(f"{ctx.author} called !about. Returning information about the bot.")          await ctx.send(embed=embed) -    @command(name="info()", aliases=["info", "about()", "about"]) -    @with_role(Roles.verified) -    async def info_wrapper(self, ctx: Context): -        """ -        Get information about the bot -        """ - -        await ctx.invoke(self.info) - -    @command(name="print()", aliases=["print", "echo", "echo()"]) +    @command(name='echo', aliases=('print',))      @with_role(Roles.owner, Roles.admin, Roles.moderator) -    async def echo_command(self, ctx: Context, text: str): +    async def echo_command(self, ctx: Context, *, text: str):          """          Send the input verbatim to the current channel          """          await ctx.send(text) -    @command(name="embed()", aliases=["embed"]) +    @command(name='embed')      @with_role(Roles.owner, Roles.admin, Roles.moderator) -    async def embed_command(self, ctx: Context, text: str): +    async def embed_command(self, ctx: Context, *, text: str):          """          Send the input within an embed to the current channel          """ diff --git a/bot/cogs/cogs.py b/bot/cogs/cogs.py index ef13aef3f..80b8607a4 100644 --- a/bot/cogs/cogs.py +++ b/bot/cogs/cogs.py @@ -2,7 +2,7 @@ import logging  import os  from discord import ClientException, Colour, Embed -from discord.ext.commands import Bot, Context, command +from discord.ext.commands import Bot, Context, group  from bot.constants import (      Emojis, Roles, URLs, @@ -36,13 +36,17 @@ class Cogs:          # Allow reverse lookups by reversing the pairs          self.cogs.update({v: k for k, v in self.cogs.items()}) -    @command(name="cogs.load()", aliases=["cogs.load", "load_cog"]) +    @group(name='cogs', aliases=('c',)) +    async def cogs_group(self, ctx: Context): +        """Load, unload, reload, and list active cogs.""" + +    @cogs_group.command(name='load', aliases=('l',))      @with_role(Roles.moderator, Roles.admin, Roles.owner, Roles.devops)      async def load_command(self, ctx: Context, cog: str):          """          Load up an unloaded cog, given the module containing it -        You can specify the cog name for any cogs that are placed directly within `bot.cogs`, or specify the +        You can specify the cog name for any cogs that are placed directly within `!cogs`, or specify the          entire module directly.          """ @@ -93,13 +97,13 @@ class Cogs:          await ctx.send(embed=embed) -    @command(name="cogs.unload()", aliases=["cogs.unload", "unload_cog"]) +    @cogs_group.command(name='unload', aliases=('ul',))      @with_role(Roles.moderator, Roles.admin, Roles.owner, Roles.devops)      async def unload_command(self, ctx: Context, cog: str):          """          Unload an already-loaded cog, given the module containing it -        You can specify the cog name for any cogs that are placed directly within `bot.cogs`, or specify the +        You can specify the cog name for any cogs that are placed directly within `!cogs`, or specify the          entire module directly.          """ @@ -145,13 +149,13 @@ class Cogs:          await ctx.send(embed=embed) -    @command(name="cogs.reload()", aliases=["cogs.reload", "reload_cog"]) +    @cogs_group.command(name='reload', aliases=('r',))      @with_role(Roles.moderator, Roles.admin, Roles.owner, Roles.devops)      async def reload_command(self, ctx: Context, cog: str):          """          Reload an unloaded cog, given the module containing it -        You can specify the cog name for any cogs that are placed directly within `bot.cogs`, or specify the +        You can specify the cog name for any cogs that are placed directly within `!cogs`, or specify the          entire module directly.          If you specify "*" as the cog, every cog currently loaded will be unloaded, and then every cog present in the @@ -250,7 +254,7 @@ class Cogs:          await ctx.send(embed=embed) -    @command(name="cogs.list()", aliases=["cogs", "cogs.list", "cogs()"]) +    @cogs_group.command(name='list', aliases=('all',))      @with_role(Roles.moderator, Roles.admin, Roles.owner, Roles.devops)      async def list_command(self, ctx: Context):          """ diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 45d5bb417..054a93e63 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -2,7 +2,7 @@ import logging  from datetime import datetime, timedelta  from discord import Colour, Embed, Member -from discord.ext.commands import Bot, Context, command +from discord.ext.commands import Bot, Context, group  from bot.cogs.modlog import ModLog  from bot.constants import Channels, Emojis, Icons, Keys, Roles, URLs @@ -97,13 +97,19 @@ class Defcon:                      message, member.avatar_url_as(static_format="png")                  ) +    @group(name='defcon', aliases=('dc',), invoke_without_command=True) +    async def defcon_group(self, ctx: Context): +        """Check the DEFCON status or run a subcommand.""" + +        await ctx.invoke(self.status_command) + +    @defcon_group.command(name='enable', aliases=('on', 'e'))      @with_role(Roles.admin, Roles.owner) -    @command(name="defcon.enable", aliases=["defcon.enable()", "defcon_enable", "defcon_enable()"]) -    async def enable(self, ctx: Context): +    async def enable_command(self, ctx: Context):          """          Enable DEFCON mode. Useful in a pinch, but be sure you know what you're doing! -        Currently, this just adds an account age requirement. Use bot.defcon.days(int) to set how old an account must +        Currently, this just adds an account age requirement. Use !defcon days <int> to set how old an account must          be, in days.          """ @@ -143,9 +149,9 @@ class Defcon:                  f"**Days:** {self.days.days}\n\n"              ) +    @defcon_group.command(name='disable', aliases=('off', 'd'))      @with_role(Roles.admin, Roles.owner) -    @command(name="defcon.disable", aliases=["defcon.disable()", "defcon_disable", "defcon_disable()"]) -    async def disable(self, ctx: Context): +    async def disable_command(self, ctx: Context):          """          Disable DEFCON mode. Useful in a pinch, but be sure you know what you're doing!          """ @@ -184,9 +190,9 @@ class Defcon:                  f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)"              ) +    @defcon_group.command(name='status', aliases=('s',))      @with_role(Roles.admin, Roles.owner) -    @command(name="defcon", aliases=["defcon()", "defcon.status", "defcon.status()"]) -    async def defcon(self, ctx: Context): +    async def status_command(self, ctx: Context):          """          Check the current status of DEFCON mode.          """ @@ -199,8 +205,8 @@ class Defcon:          await ctx.send(embed=embed) +    @defcon_group.command(name='days')      @with_role(Roles.admin, Roles.owner) -    @command(name="defcon.days", aliases=["defcon.days()", "defcon_days", "defcon_days()"])      async def days_command(self, ctx: Context, days: int):          """          Set how old an account must be to join the server, in days, with DEFCON mode enabled. diff --git a/bot/cogs/deployment.py b/bot/cogs/deployment.py index ca42fd980..72e1a5d92 100644 --- a/bot/cogs/deployment.py +++ b/bot/cogs/deployment.py @@ -1,7 +1,7 @@  import logging  from discord import Colour, Embed -from discord.ext.commands import Bot, Context, command +from discord.ext.commands import Bot, Context, command, group  from bot.constants import Keys, Roles, URLs  from bot.decorators import with_role @@ -17,9 +17,13 @@ class Deployment:      def __init__(self, bot: Bot):          self.bot = bot -    @command(name="redeploy()", aliases=["bot.redeploy", "bot.redeploy()", "redeploy"]) +    @group(name='redeploy') +    async def redeploy_group(self, ctx: Context): +        """Redeploy the bot or the site.""" + +    @redeploy_group.command(name='bot')      @with_role(Roles.admin, Roles.owner, Roles.devops) -    async def redeploy(self, ctx: Context): +    async def bot_command(self, ctx: Context):          """          Trigger bot deployment on the server - will only redeploy if there were changes to deploy          """ @@ -34,9 +38,9 @@ class Deployment:              log.error(f"{ctx.author} triggered deployment for bot. Deployment failed to start.")              await ctx.send(f"{ctx.author.mention} Bot deployment failed - check the logs!") -    @command(name="deploy_site()", aliases=["bot.deploy_site", "bot.deploy_site()", "deploy_site"]) +    @redeploy_group.command(name='site')      @with_role(Roles.admin, Roles.owner, Roles.devops) -    async def deploy_site(self, ctx: Context): +    async def site_command(self, ctx: Context):          """          Trigger website deployment on the server - will only redeploy if there were changes to deploy          """ @@ -51,9 +55,9 @@ class Deployment:              log.error(f"{ctx.author} triggered deployment for site. Deployment failed to start.")              await ctx.send(f"{ctx.author.mention} Site deployment failed - check the logs!") -    @command(name="uptimes()", aliases=["bot.uptimes", "bot.uptimes()", "uptimes"]) +    @command(name='uptimes')      @with_role(Roles.admin, Roles.owner, Roles.devops) -    async def uptimes(self, ctx: Context): +    async def uptimes_command(self, ctx: Context):          """          Check the various deployment uptimes for each service          """ diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index e6d108720..2b310f11c 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -356,7 +356,13 @@ class Doc:              changes = await resp.json()              return changes["deleted"] == 1  # Did the package delete successfully? -    @commands.command(name='docs.get()', aliases=['docs.get']) +    @commands.group(name='docs', aliases=('doc', 'd'), invoke_without_command=True) +    async def docs_group(self, ctx, symbol: commands.clean_content = None): +        """Lookup documentation for Python symbols.""" + +        await ctx.invoke(self.get_command) + +    @docs_group.command(name='get', aliases=('g',))      async def get_command(self, ctx, symbol: commands.clean_content = None):          """          Return a documentation embed for a given symbol. @@ -367,8 +373,10 @@ class Doc:                         or nothing to get a list of all inventories          Examples: -            bot.docs.get('aiohttp') -            bot.docs['aiohttp'] +            !docs +            !docs aiohttp +            !docs aiohttp.ClientSession +            !docs get aiohttp.ClientSession          """          if symbol is None: @@ -396,8 +404,8 @@ class Doc:              else:                  await ctx.send(embed=doc_embed) +    @docs_group.command(name='set', aliases=('s',))      @with_role(Roles.admin, Roles.owner, Roles.moderator) -    @commands.command(name='docs.set()', aliases=['docs.set'])      async def set_command(          self, ctx, package_name: ValidPythonIdentifier,          base_url: ValidURL, inventory_url: InventoryURL @@ -413,11 +421,10 @@ class Doc:          :param inventory_url: The intersphinx inventory URL.          Example: -            bot.docs.set( -                'discord', -                'https://discordpy.readthedocs.io/en/rewrite/', -                'https://discordpy.readthedocs.io/en/rewrite/objects.inv' -            ) +            !docs set \ +                    discord \ +                    https://discordpy.readthedocs.io/en/rewrite/ \ +                    https://discordpy.readthedocs.io/en/rewrite/objects.inv          """          await self.set_package(package_name, base_url, inventory_url) @@ -435,8 +442,8 @@ class Doc:              await self.refresh_inventory()          await ctx.send(f"Added package `{package_name}` to database and refreshed inventory.") +    @docs_group.command(name='delete', aliases=('remove', 'rm', 'd'))      @with_role(Roles.admin, Roles.owner, Roles.moderator) -    @commands.command(name='docs.delete()', aliases=['docs.delete', 'docs.remove()', 'docs.remove'])      async def delete_command(self, ctx, package_name: ValidPythonIdentifier):          """          Removes the specified package from the database. @@ -445,8 +452,7 @@ class Doc:          :param package_name: The package name, for example `aiohttp`.          Examples: -            bot.tags.delete('aiohttp') -            bot.tags['aiohttp'] = None +            !docs delete aiohttp          """          success = await self.delete_package(package_name) diff --git a/bot/cogs/eval.py b/bot/cogs/eval.py index ddd5c558a..6506a5b9b 100644 --- a/bot/cogs/eval.py +++ b/bot/cogs/eval.py @@ -8,7 +8,7 @@ import traceback  from io import StringIO  import discord -from discord.ext.commands import Bot, command +from discord.ext.commands import Bot, group  from bot.constants import Roles  from bot.decorators import with_role @@ -173,7 +173,11 @@ async def func():  # (None,) -> Any          out, embed = self._format(code, res)          await ctx.send(f"```py\n{out}```", embed=embed) -    @command(name="internal.eval()", aliases=["internal.eval"]) +    @group(name='internal', aliases=('int',)) +    async def internal_group(self, ctx): +        """Internal commands. Top secret!""" + +    @internal_group.command(name='eval', aliases=('e',))      @with_role(Roles.admin, Roles.owner)      async def eval(self, ctx, *, code: str):          """ Run eval in a REPL-like format. """ diff --git a/bot/cogs/events.py b/bot/cogs/events.py index 85fec3aa3..a7111b8a0 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -17,9 +17,7 @@ log = logging.getLogger(__name__)  class Events: -    """ -    No commands, just event handlers -    """ +    """No commands, just event handlers."""      def __init__(self, bot: Bot):          self.bot = bot diff --git a/bot/cogs/hiphopify.py b/bot/cogs/hiphopify.py index 00c79809f..785aedca2 100644 --- a/bot/cogs/hiphopify.py +++ b/bot/cogs/hiphopify.py @@ -75,9 +75,9 @@ class Hiphopify:                      "to DM them, and a discord.errors.Forbidden error was incurred."                  ) +    @command(name='hiphopify', aliases=('force_nick', 'hh'))      @with_role(Roles.admin, Roles.owner, Roles.moderator) -    @command(name="hiphopify()", aliases=["hiphopify", "force_nick()", "force_nick"]) -    async def hiphopify(self, ctx: Context, member: Member, duration: str, forced_nick: str = None): +    async def hiphopify(self, ctx: Context, member: Member, duration: str, *, forced_nick: str = None):          """          This command will force a random rapper name (like Lil' Wayne) to be the users          nickname for a specified duration. If a forced_nick is provided, it will use that instead. @@ -151,8 +151,8 @@ class Hiphopify:              await member.edit(nick=forced_nick)              await ctx.send(embed=embed) +    @command(name='unhiphopify', aliases=('release_nick', 'uhh'))      @with_role(Roles.admin, Roles.owner, Roles.moderator) -    @command(name="unhiphopify()", aliases=["unhiphopify", "release_nick()", "release_nick"])      async def unhiphopify(self, ctx: Context, member: Member):          """          This command will remove the entry from our database, allowing the user diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index 90510c8c4..cc0373232 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -3,7 +3,7 @@ import logging  from datetime import datetime, timedelta  from discord import Colour, Embed -from discord.ext.commands import BadArgument, Bot, Context, Converter, command +from discord.ext.commands import BadArgument, Bot, Context, Converter, group  from bot.constants import Channels, Keys, Roles, URLs  from bot.decorators import with_role @@ -83,9 +83,13 @@ class OffTopicNames:              coro = update_names(self.bot, self.headers)              self.updater_task = await self.bot.loop.create_task(coro) -    @command(name='otname.add()', aliases=['otname.add']) +    @group(name='otname', aliases=('otnames', 'otn')) +    async def otname_group(self, ctx): +        """Add or list items from the off-topic channel name rotation.""" + +    @otname_group.command(name='add', aliases=('a',))      @with_role(Roles.owner, Roles.admin, Roles.moderator) -    async def otname_add(self, ctx, name: OffTopicName): +    async def add_command(self, ctx, name: OffTopicName):          """Adds a new off-topic name to the rotation."""          result = await self.bot.http_session.post( @@ -106,9 +110,9 @@ class OffTopicNames:              error_reason = response.get('message', "No reason provided.")              await ctx.send(f":warning: got non-200 from the API: {error_reason}") -    @command(name='otname.list()', aliases=['otname.list']) +    @otname_group.command(name='list', aliases=('l',))      @with_role(Roles.owner, Roles.admin, Roles.moderator) -    async def otname_list(self, ctx): +    async def list_command(self, ctx):          """          Lists all currently known off-topic channel names in a paginator.          Restricted to Moderator and above to not spoil the surprise. diff --git a/bot/cogs/snakes.py b/bot/cogs/snakes.py index ec32a119d..f83f8e354 100644 --- a/bot/cogs/snakes.py +++ b/bot/cogs/snakes.py @@ -14,7 +14,7 @@ from typing import Any, Dict  import aiohttp  import async_timeout  from discord import Colour, Embed, File, Member, Message, Reaction -from discord.ext.commands import BadArgument, Bot, Context, bot_has_permissions, command +from discord.ext.commands import BadArgument, Bot, Context, bot_has_permissions, group  from PIL import Image, ImageDraw, ImageFont  from bot.constants import ERROR_REPLIES, Keys, URLs @@ -462,10 +462,14 @@ class Snakes:      # endregion      # region: Commands +    @group(name='snakes', aliases=('snake',)) +    async def snakes_group(self, ctx: Context): +        """Commands from our first code jam.""" +      @bot_has_permissions(manage_messages=True) -    @command(name="snakes.antidote()", aliases=["snakes.antidote"]) +    @snakes_group.command(name='antidote')      @locked() -    async def antidote(self, ctx: Context): +    async def antidote_command(self, ctx: Context):          """          Antidote - Can you create the antivenom before the patient dies? @@ -604,8 +608,8 @@ class Snakes:          log.debug("Ending pagination and removing all reactions...")          await board_id.clear_reactions() -    @command(name="snakes.draw()", aliases=["snakes.draw"]) -    async def draw(self, ctx: Context): +    @snakes_group.command(name='draw') +    async def draw_command(self, ctx: Context):          """          Draws a random snek using Perlin noise @@ -648,10 +652,10 @@ class Snakes:              await ctx.send(file=file) -    @command(name="snakes.get()", aliases=["snakes.get"]) +    @snakes_group.command(name='get')      @bot_has_permissions(manage_messages=True)      @locked() -    async def get(self, ctx: Context, name: Snake = None): +    async def get_command(self, ctx: Context, *, name: Snake = None):          """          Fetches information about a snake from Wikipedia.          :param ctx: Context object passed from discord.py @@ -699,9 +703,9 @@ class Snakes:              await ctx.send(embed=embed) -    @command(name="snakes.guess()", aliases=["snakes.guess", "identify"]) +    @snakes_group.command(name='guess', aliases=('identify',))      @locked() -    async def guess(self, ctx): +    async def guess_command(self, ctx):          """          Snake identifying game! @@ -733,8 +737,8 @@ class Snakes:          options = {f"{'abcd'[snakes.index(snake)]}": snake for snake in snakes}          await self._validate_answer(ctx, guess, answer, options) -    @command(name="snakes.hatch()", aliases=["snakes.hatch", "hatch"]) -    async def hatch(self, ctx: Context): +    @snakes_group.command(name='hatch') +    async def hatch_command(self, ctx: Context):          """          Hatches your personal snake @@ -765,8 +769,8 @@ class Snakes:          await ctx.channel.send(embed=my_snake_embed) -    @command(name="snakes.movie()", aliases=["snakes.movie"]) -    async def movie(self, ctx: Context): +    @snakes_group.command(name='movie') +    async def movie_command(self, ctx: Context):          """          Gets a random snake-related movie from OMDB. @@ -835,9 +839,9 @@ class Snakes:              embed=embed          ) -    @command(name="snakes.quiz()", aliases=["snakes.quiz"]) +    @snakes_group.command(name='quiz')      @locked() -    async def quiz(self, ctx: Context): +    async def quiz_command(self, ctx: Context):          """          Asks a snake-related question in the chat and validates the user's guess. @@ -863,8 +867,8 @@ class Snakes:          quiz = await ctx.channel.send("", embed=embed)          await self._validate_answer(ctx, quiz, answer, options) -    @command(name="snakes.name()", aliases=["snakes.name", "snakes.name_gen", "snakes.name_gen()"]) -    async def random_snake_name(self, ctx: Context, name: str = None): +    @snakes_group.command(name='name', aliases=('name_gen',)) +    async def name_command(self, ctx: Context, *, name: str = None):          """          Slices the users name at the last vowel (or second last if the name          ends with a vowel), and then combines it with a random snake name, @@ -933,9 +937,9 @@ class Snakes:          return await ctx.send(embed=embed) -    @command(name="snakes.sal()", aliases=["snakes.sal"]) +    @snakes_group.command(name='sal')      @locked() -    async def sal(self, ctx: Context): +    async def sal_command(self, ctx: Context):          """          Play a game of Snakes and Ladders! @@ -953,8 +957,8 @@ class Snakes:          await game.open_game() -    @command(name="snakes.about()", aliases=["snakes.about"]) -    async def snake_about(self, ctx: Context): +    @snakes_group.command(name='about') +    async def about_command(self, ctx: Context):          """          A command that shows an embed with information about the event,          it's participants, and its winners. @@ -986,8 +990,8 @@ class Snakes:                  "48 hours. The staff then selected the best features from all the best teams, and made modifications "                  "to ensure they would all work together before integrating them into the community bot.\n\n"                  "It was a tight race, but in the end, <@!104749643715387392> and <@!303940835005825024> " -                "walked away as grand champions. Make sure you check out `bot.snakes.sal()`, `bot.snakes.draw()` " -                "and `bot.snakes.hatch()` to see what they came up with." +                "walked away as grand champions. Make sure you check out `!snakes sal`, `!snakes draw` " +                "and `!snakes hatch` to see what they came up with."              )          ) @@ -1000,8 +1004,8 @@ class Snakes:          await ctx.channel.send(embed=embed) -    @command(name="snakes.card()", aliases=["snakes.card"]) -    async def snake_card(self, ctx: Context, name: Snake = None): +    @snakes_group.command(name='card') +    async def card_command(self, ctx: Context, *, name: Snake = None):          """          Create an interesting little card from a snake! @@ -1039,8 +1043,8 @@ class Snakes:              file=File(final_buffer, filename=content['name'].replace(" ", "") + ".png")          ) -    @command(name="snakes.fact()", aliases=["snakes.fact"]) -    async def snake_fact(self, ctx: Context): +    @snakes_group.command(name='fact') +    async def fact_command(self, ctx: Context):          """          Gets a snake-related fact @@ -1060,8 +1064,8 @@ class Snakes:          )          await ctx.channel.send(embed=embed) -    @command(name="snakes()", aliases=["snakes"]) -    async def snake_help(self, ctx: Context): +    @snakes_group.command(name='help') +    async def help_command(self, ctx: Context):          """          This just invokes the help command on this cog.          """ @@ -1069,8 +1073,8 @@ class Snakes:          log.debug(f"{ctx.author} requested info about the snakes cog")          return await ctx.invoke(self.bot.get_command("help"), "Snakes") -    @command(name="snakes.snakify()", aliases=["snakes.snakify"]) -    async def snakify(self, ctx: Context, message: str = None): +    @snakes_group.command(name='snakify') +    async def snakify_command(self, ctx: Context, *, message: str = None):          """          How would I talk if I were a snake?          :param ctx: context @@ -1112,8 +1116,8 @@ class Snakes:              await ctx.channel.send(embed=embed) -    @command(name="snakes.video()", aliases=["snakes.video", "snakes.get_video()", "snakes.get_video"]) -    async def video(self, ctx: Context, search: str = None): +    @snakes_group.command(name='video', aliases=('get_video',)) +    async def video_command(self, ctx: Context, *, search: str = None):          """          Gets a YouTube video about snakes          :param name: Optional, a name of a snake. Used to search for videos with that name @@ -1153,8 +1157,8 @@ class Snakes:          else:              log.warning(f"YouTube API error. Full response looks like {response}") -    @command(name="snakes.zen()", aliases=["zen"]) -    async def zen(self, ctx: Context): +    @snakes_group.command(name='zen') +    async def zen_command(self, ctx: Context):          """          Gets a random quote from the Zen of Python,          except as if spoken by a snake. @@ -1180,9 +1184,9 @@ class Snakes:      # endregion      # region: Error handlers -    @get.error -    @snake_card.error -    @video.error +    @get_command.error +    @card_command.error +    @video_command.error      async def command_error(self, ctx, error):          embed = Embed() diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 69a2ed59e..7115e3ae5 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -63,16 +63,13 @@ class Snekbox:      def rmq(self) -> RMQ:          return self.bot.get_cog("RMQ") -    @command(name="snekbox.eval()", aliases=["snekbox.eval", "eval()", "eval"]) +    @command(name='eval', aliases=('e',))      @guild_only()      @check(channel_is_whitelisted_or_author_can_bypass) -    async def do_eval(self, ctx: Context, code: str): +    async def eval_command(self, ctx: Context, *, code: str):          """          Run some code. get the result back. We've done our best to make this safe, but do let us know if you          manage to find an issue with it! - -        Remember, your code must be within some kind of string. Why not surround your code with quotes or put it in -        a docstring?          """          if ctx.author.id in self.jobs: @@ -159,7 +156,7 @@ class Snekbox:              del self.jobs[ctx.author.id]              raise -    @do_eval.error +    @eval_command.error      async def eval_command_error(self, ctx: Context, error: CommandError):          embed = Embed(colour=Colour.red()) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 82e009bf8..afdd6c1dc 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -5,7 +5,7 @@ import time  from discord import Colour, Embed  from discord.ext.commands import (      BadArgument, Bot, -    Context, Converter, command +    Context, Converter, group  )  from bot.constants import ( @@ -150,19 +150,14 @@ class Tags:          return tag_data -    @command(name="tags()", aliases=["tags"], hidden=True) -    async def info_command(self, ctx: Context): -        """ -        Show available methods for this class. - -        :param ctx: Discord message context -        """ +    @group(name='tags', aliases=('tag', 't'), hidden=True, invoke_without_command=True) +    async def tags_group(self, ctx: Context): +        """Show all known tags, a single tag, or run a subcommand.""" -        log.debug(f"{ctx.author} requested info about the tags cog") -        return await ctx.invoke(self.bot.get_command("help"), "Tags") +        await ctx.invoke(self.get_command) -    @command(name="tags.get()", aliases=["tags.get", "tags.show()", "tags.show", "get_tag"]) -    async def get_command(self, ctx: Context, tag_name: TagNameConverter=None): +    @tags_group.command(name='get', aliases=('show', 'g')) +    async def get_command(self, ctx: Context, *, tag_name: TagNameConverter=None):          """          Get a list of all tags or a specified tag. @@ -244,32 +239,25 @@ class Tags:                  embed.description = "**There are no tags in the database!**"              if tag_name: -                embed.set_footer(text="To show a list of all tags, use bot.tags.get().") +                embed.set_footer(text="To show a list of all tags, use !tags.")                  embed.title = "Tag not found."          # Paginate if this is a list of all tags          if tags: -            if ctx.invoked_with == "tags.keys()": -                detail_invocation = "bot.tags[<tagname>]" -            elif ctx.invoked_with == "tags.get()": -                detail_invocation = "bot.tags.get(<tagname>)" -            else: -                detail_invocation = "bot.tags.get <tagname>" -              log.debug(f"Returning a paginated list of all tags.")              return await LinePaginator.paginate(                  (lines for lines in tags),                  ctx, embed, -                footer_text=f"To show a tag, type {detail_invocation}.", +                footer_text="To show a tag, type !tags <tagname>.",                  empty=False,                  max_lines=15              )          return await ctx.send(embed=embed) +    @tags_group.command(name='set', aliases=('add', 'edit', 's'))      @with_role(Roles.admin, Roles.owner, Roles.moderator) -    @command(name="tags.set()", aliases=["tags.set", "tags.add", "tags.add()", "tags.edit", "tags.edit()", "add_tag"]) -    async def set_command(self, ctx: Context, tag_name: TagNameConverter, tag_content: TagContentConverter): +    async def set_command(self, ctx: Context, tag_name: TagNameConverter, *, tag_content: TagContentConverter):          """          Create a new tag or edit an existing one. @@ -303,9 +291,9 @@ class Tags:          return await ctx.send(embed=embed) +    @tags_group.command(name='delete', aliases=('remove', 'rm', 'd'))      @with_role(Roles.admin, Roles.owner) -    @command(name="tags.delete()", aliases=["tags.delete", "tags.remove", "tags.remove()", "remove_tag"]) -    async def delete_command(self, ctx: Context, tag_name: TagNameConverter): +    async def delete_command(self, ctx: Context, *, tag_name: TagNameConverter):          """          Remove a tag from the database. @@ -353,16 +341,6 @@ class Tags:          else:              log.error(f"Unhandled tag command error: {error} ({error.original})") -    @command(name="tags.keys()") -    async def keys_command(self, ctx: Context): -        """ -        Alias for `tags.get()` with no arguments. - -        :param ctx: discord message context -        """ - -        return await ctx.invoke(self.get_command) -  def setup(bot):      bot.add_cog(Tags(bot)) diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index 7b11f521c..cfd196e31 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -23,9 +23,9 @@ class Utils:          self.base_pep_url = "http://www.python.org/dev/peps/pep-"          self.base_github_pep_url = "https://raw.githubusercontent.com/python/peps/master/pep-" -    @command(name="pep()", aliases=["pep", "get_pep"]) +    @command(name='pep', aliases=('get_pep', 'p'))      @with_role(Roles.verified) -    async def pep_search(self, ctx: Context, pep_number: str): +    async def pep_command(self, ctx: Context, pep_number: str):          """          Fetches information about a PEP and sends it to the channel.          """ diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index 621610903..b0667fdd0 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -21,10 +21,10 @@ your information removed here as well.  Feel free to review them at any point!  Additionally, if you'd like to receive notifications for the announcements we post in <#{Channels.announcements}> \ -from time to time, you can send `self.subscribe()` to <#{Channels.bot}> at any time to assign yourself the \ +from time to time, you can send `!subscribe` to <#{Channels.bot}> at any time to assign yourself the \  **Announcements** role. We'll mention this role every time we make an announcement. -If you'd like to unsubscribe from the announcement notifications, simply send `self.unsubscribe()` to <#{Channels.bot}>. +If you'd like to unsubscribe from the announcement notifications, simply send `!unsubscribe` to <#{Channels.bot}>.  """ @@ -59,7 +59,7 @@ class Verification:              log.debug(f"{ctx.author} posted '{ctx.message.content}' in the verification "                        "channel. We are providing instructions how to verify.")              await ctx.send( -                f"{ctx.author.mention} Please type `self.accept()` to verify that you accept our rules, " +                f"{ctx.author.mention} Please type `!accept` to verify that you accept our rules, "                  f"and gain access to the rest of the server.",                  delete_after=20              ) @@ -71,15 +71,15 @@ class Verification:              except NotFound:                  log.trace("No message found, it must have been deleted by another bot.") -    @command(name="accept", hidden=True, aliases=["verify", "verified", "accepted", "accept()"]) +    @command(name='accept', aliases=('verify', 'verified', 'accepted'), hidden=True)      @without_role(Roles.verified)      @in_channel(Channels.verification) -    async def accept(self, ctx: Context, *_):  # We don't actually care about the args +    async def accept_command(self, ctx: Context, *_):  # We don't actually care about the args          """          Accept our rules and gain access to the rest of the server          """ -        log.debug(f"{ctx.author} called self.accept(). Assigning the 'Developer' role.") +        log.debug(f"{ctx.author} called !accept. Assigning the 'Developer' role.")          await ctx.author.add_roles(Object(Roles.verified), reason="Accepted the rules")          try:              await ctx.author.send(WELCOME_MESSAGE) @@ -95,9 +95,9 @@ class Verification:          except NotFound:              log.trace("No message found, it must have been deleted by another bot.") -    @command(name="subscribe", aliases=["subscribe()"]) +    @command(name='subscribe')      @in_channel(Channels.bot) -    async def subscribe(self, ctx: Context, *_):  # We don't actually care about the args +    async def subscribe_command(self, ctx: Context, *_):  # We don't actually care about the args          """          Subscribe to announcement notifications by assigning yourself the role          """ @@ -114,7 +114,7 @@ class Verification:                  f"{ctx.author.mention} You're already subscribed!",              ) -        log.debug(f"{ctx.author} called self.subscribe(). Assigning the 'Announcements' role.") +        log.debug(f"{ctx.author} called !subscribe. Assigning the 'Announcements' role.")          await ctx.author.add_roles(Object(Roles.announcements), reason="Subscribed to announcements")          log.trace(f"Deleting the message posted by {ctx.author}.") @@ -123,9 +123,9 @@ class Verification:              f"{ctx.author.mention} Subscribed to <#{Channels.announcements}> notifications.",          ) -    @command(name="unsubscribe", aliases=["unsubscribe()"]) +    @command(name='unsubscribe')      @in_channel(Channels.bot) -    async def unsubscribe(self, ctx: Context, *_):  # We don't actually care about the args +    async def unsubscribe_command(self, ctx: Context, *_):  # We don't actually care about the args          """          Unsubscribe from announcement notifications by removing the role from yourself          """ @@ -142,7 +142,7 @@ class Verification:                  f"{ctx.author.mention} You're already unsubscribed!"              ) -        log.debug(f"{ctx.author} called self.unsubscribe(). Removing the 'Announcements' role.") +        log.debug(f"{ctx.author} called !unsubscribe. Removing the 'Announcements' role.")          await ctx.author.remove_roles(Object(Roles.announcements), reason="Unsubscribed from announcements")          log.trace(f"Deleting the message posted by {ctx.author}.") diff --git a/bot/formatter.py b/bot/formatter.py deleted file mode 100644 index 5ec23dcb2..000000000 --- a/bot/formatter.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -Credit to Rapptz's script used as an example: -https://github.com/Rapptz/discord.py/blob/rewrite/discord/ext/commands/formatter.py -Which falls under The MIT License. -""" - -import itertools -import logging -from inspect import formatargspec, getfullargspec - -from discord.ext.commands import Command, HelpFormatter, Paginator - -from bot.constants import Bot - -log = logging.getLogger(__name__) - - -class Formatter(HelpFormatter): -    def __init__(self, *args, **kwargs): -        super().__init__(*args, **kwargs) - -    def _add_subcommands_to_page(self, max_width: int, commands: list): -        """ -        basically the same function from d.py but changed: -        - to make the helptext appear as a comment -        - to change the indentation to the PEP8 standard: 4 spaces -        """ - -        for name, command in commands: -            if name in command.aliases: -                # skip aliases -                continue - -            entry = "    {0}{1:<{width}} # {2}".format(Bot.help_prefix, name, command.short_doc, width=max_width) -            shortened = self.shorten(entry) -            self._paginator.add_line(shortened) - -            if name.endswith('get()'): -                alternate_syntax_entry = "    {0}{1:<{width}} # {2}".format( -                    Bot.help_prefix, name.split('.')[0] + '[<arg>]', -                    f"Alternative syntax for {name}", width=max_width -                ) -                self._paginator.add_line(self.shorten(alternate_syntax_entry)) - -    async def format(self): -        """ -        rewritten help command to make it more python-y - -        example of specific command: -        async def <command>(ctx, <args>): -            \""" -            <help text> -            \""" -            await do_<command>(ctx, <args>) - -        example of standard help page: -        class <cog1>: -            bot.<command1>() # <command1 help> -        class <cog2>: -            bot.<command2>() # <command2 help> - -        # <ending help note> -        """ - -        self._paginator = Paginator(prefix="```py") - -        if isinstance(self.command, Command): -            # string used purely to make logs a teensy bit more readable -            cog_string = f" from {self.command.cog_name}" if self.command.cog_name else "" - -            log.trace(f"Help command is on specific command {self.command.name}{cog_string}.") - -            # strip the command off bot. and () -            stripped_command = self.command.name.replace(Bot.help_prefix, "").replace("()", "") - -            # get the args using the handy inspect module -            argspec = getfullargspec(self.command.callback) -            arguments = formatargspec(*argspec) - -            for annotation in argspec.annotations.values(): -                # remove module name to only show class name -                # discord.ext.commands.context.Context -> Context -                arguments = arguments.replace(f"{annotation.__module__}.", "") - -            log.trace(f"Acquired arguments for command: '{arguments}' ") - -            # manipulate the argspec to make it valid python when 'calling' the do_<command> -            args_no_type_hints = argspec.args -            for kwarg in argspec.kwonlyargs: -                args_no_type_hints.append("{0}={0}".format(kwarg)) -            args_no_type_hints = "({0})".format(", ".join(args_no_type_hints)) - -            # remove self from the args -            arguments = arguments.replace("self, ", "") -            args_no_type_hints = args_no_type_hints.replace("self, ", "") - -            # indent every line in the help message -            helptext = "\n    ".join(self.command.help.split("\n")) - -            # prepare the different sections of the help output, and add them to the paginator -            definition = f"async def {stripped_command}{arguments}:" -            doc_elems = [ -                '"""', -                helptext, -                '"""' -            ] - -            docstring = "" -            for elem in doc_elems: -                docstring += f'    {elem}\n' - -            invocation = f"    await do_{stripped_command}{args_no_type_hints}" -            self._paginator.add_line(definition) -            self._paginator.add_line(docstring) -            self._paginator.add_line(invocation) - -            log.trace(f"Help for {self.command.name}{cog_string} added to paginator.") - -            log.debug(f"Help for {self.command.name}{cog_string} generated.") - -            return self._paginator.pages - -        max_width = self.max_name_size - -        def category_check(tup): -            cog = tup[1].cog_name -            # zero width character to make it appear last when put in alphabetical order -            return cog if cog is not None else "Bot" - -        command_list = await self.filter_command_list() -        data = sorted(command_list, key=category_check) - -        log.trace(f"Acquired command list and sorted by cog name: {[command[1].name for command in data]}") - -        for category, commands in itertools.groupby(data, key=category_check): -            commands = sorted(commands) -            if len(commands) > 0: -                self._paginator.add_line(f"class {category}:") -                self._add_subcommands_to_page(max_width, commands) - -        log.trace("Added cog and command names to the paginator.") - -        self._paginator.add_line() -        ending_note = self.get_ending_note() -        # make the ending note appear as comments -        ending_note = "# "+ending_note.replace("\n", "\n# ") -        self._paginator.add_line(ending_note) - -        log.trace("Added ending note to paginator.") -        log.debug("General or Cog help generated.") - -        return self._paginator.pages | 
