diff options
| author | 2018-07-27 19:01:08 -0400 | |
|---|---|---|
| committer | 2018-07-27 19:01:08 -0400 | |
| commit | 9dcbd76682a458a238784014279448b3e6aa9065 (patch) | |
| tree | d264a74d4a72219e4a092d7411bbbb1e207f0482 | |
| parent | Simplify ban line (diff) | |
| parent | Icon updates (diff) | |
Merge branch 'master' into feature/rowboat-replacement
| -rw-r--r-- | bot/__init__.py | 218 | ||||
| -rw-r--r-- | bot/__main__.py | 14 | ||||
| -rw-r--r-- | bot/cogs/bigbrother.py | 13 | ||||
| -rw-r--r-- | bot/cogs/bot.py | 25 | ||||
| -rw-r--r-- | bot/cogs/cogs.py | 21 | ||||
| -rw-r--r-- | bot/cogs/defcon.py | 138 | ||||
| -rw-r--r-- | bot/cogs/deployment.py | 19 | ||||
| -rw-r--r-- | bot/cogs/doc.py | 30 | ||||
| -rw-r--r-- | bot/cogs/eval.py | 9 | ||||
| -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 | 15 | ||||
| -rw-r--r-- | bot/cogs/snakes.py | 80 | ||||
| -rw-r--r-- | bot/cogs/snekbox.py | 22 | ||||
| -rw-r--r-- | bot/cogs/tags.py | 48 | ||||
| -rw-r--r-- | bot/cogs/utils.py | 6 | ||||
| -rw-r--r-- | bot/cogs/verification.py | 24 | ||||
| -rw-r--r-- | bot/constants.py | 9 | ||||
| -rw-r--r-- | bot/formatter.py | 152 | ||||
| -rw-r--r-- | config-default.yml | 13 |
20 files changed, 303 insertions, 563 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 ceab87f72..b9e6001ac 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="Help: 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..9ea8efdb0 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,12 @@ class BigBrother: await channel.send(relay_content) - @command(name='bigbrother.watched()', aliases=('bigbrother.watched',)) + @group(name='bigbrother', aliases=('bb',)) + @with_role(Roles.owner, Roles.admin, Roles.moderator) + 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 +122,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 +161,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..780850b5a 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,18 @@ 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',)) + @with_role(Roles.moderator, Roles.admin, Roles.owner, Roles.devops) + 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 +98,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 +150,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 +255,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 ea50bdf63..8ca59b058 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -2,13 +2,17 @@ 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.constants import Channels, Keys, Roles, URLs +from bot.cogs.modlog import ModLog +from bot.constants import Channels, Emojis, Icons, Keys, Roles, URLs from bot.decorators import with_role log = logging.getLogger(__name__) +COLOUR_RED = Colour(0xcd6d6d) +COLOUR_GREEN = Colour(0x68c290) + REJECTION_MESSAGE = """ Hi, {user} - Thanks for your interest in our server! @@ -31,6 +35,10 @@ class Defcon: self.days = timedelta(days=0) self.headers = {"X-API-KEY": Keys.site_api} + @property + def modlog(self) -> ModLog: + return self.bot.get_cog("ModLog") + async def on_ready(self): try: response = await self.bot.http_session.get( @@ -65,20 +73,44 @@ class Defcon: if now - member.created_at < self.days: log.info(f"Rejecting user {member}: Account is too new and DEFCON is enabled") + message_sent = False + try: await member.send(REJECTION_MESSAGE.format(user=member.mention)) + + message_sent = True except Exception: log.exception(f"Unable to send rejection message to user: {member}") await member.kick(reason="DEFCON active, user is too new") + message = ( + f"{member.name}#{member.discriminator} (`{member.id}`) " + f"was denied entry because their account is too new." + ) + + if not message_sent: + message = f"{message}\n\nUnable to send rejection message via DM; they probably have DMs disabled." + + await self.modlog.send_log_message( + Icons.defcon_denied, COLOUR_RED, "Entry denied", + message, member.avatar_url_as(static_format="png") + ) + + @group(name='defcon', aliases=('dc',), invoke_without_command=True) @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 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) + 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. """ @@ -92,15 +124,35 @@ class Defcon: ) await response.json() - except Exception: + except Exception as e: log.exception("Unable to update DEFCON settings.") - await ctx.send("DEFCON enabled locally, but there was a problem updating the site.") + await ctx.send( + f"{Emojis.defcon_enabled} DEFCON enabled.\n\n" + "**There was a problem updating the site** - This setting may be reverted when the bot is " + "restarted.\n\n" + f"```py\n{e}\n```" + ) + + await self.modlog.send_log_message( + Icons.defcon_enabled, COLOUR_GREEN, "DEFCON enabled", + f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" + f"**Days:** {self.days.days}\n\n" + "**There was a problem updating the site** - This setting may be reverted when the bot is " + "restarted.\n\n" + f"```py\n{e}\n```" + ) else: - await ctx.send("DEFCON enabled.") + await ctx.send(f"{Emojis.defcon_enabled} DEFCON enabled.") + await self.modlog.send_log_message( + Icons.defcon_enabled, COLOUR_GREEN, "DEFCON enabled", + f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" + 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! """ @@ -115,27 +167,47 @@ class Defcon: ) await response.json() - except Exception: + except Exception as e: log.exception("Unable to update DEFCON settings.") - await ctx.send("DEFCON disabled locally, but there was a problem updating the site.") + await ctx.send( + f"{Emojis.defcon_disabled} DEFCON disabled.\n\n" + "**There was a problem updating the site** - This setting may be reverted when the bot is " + "restarted.\n\n" + f"```py\n{e}\n```" + ) + + await self.modlog.send_log_message( + Icons.defcon_disabled, COLOUR_RED, "DEFCON disabled", + f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" + "**There was a problem updating the site** - This setting may be reverted when the bot is " + "restarted.\n\n" + f"```py\n{e}\n```" + ) else: - await ctx.send("DEFCON disabled.") + await ctx.send(f"{Emojis.defcon_disabled} DEFCON disabled.") + + await self.modlog.send_log_message( + Icons.defcon_disabled, COLOUR_RED, "DEFCON disabled", + 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. """ - embed = Embed(colour=Colour.blurple(), title="DEFCON Status") - embed.add_field(name="Enabled", value=str(self.enabled), inline=True) - embed.add_field(name="Days", value=str(self.days.days), inline=True) + embed = Embed( + colour=Colour.blurple(), title="DEFCON Status", + description=f"**Enabled:** {self.enabled}\n" + f"**Days:** {self.days.days}" + ) 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. @@ -151,14 +223,34 @@ class Defcon: ) await response.json() - except Exception: + except Exception as e: log.exception("Unable to update DEFCON settings.") await ctx.send( - f"DEFCON days updated; accounts must be {days} days old to join to the server " - f"- but there was a problem updating the site." + f"{Emojis.defcon_updated} DEFCON days updated; accounts must be {days} " + f"days old to join to the server.\n\n" + "**There was a problem updating the site** - This setting may be reverted when the bot is " + "restarted.\n\n" + f"```py\n{e}\n```" + ) + + await self.modlog.send_log_message( + Icons.defcon_updated, Colour.blurple(), "DEFCON updated", + f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" + f"**Days:** {self.days.days}\n\n" + "**There was a problem updating the site** - This setting may be reverted when the bot is " + "restarted.\n\n" + f"```py\n{e}\n```" ) else: - await ctx.send(f"DEFCON days updated; accounts must be {days} days old to join to the server") + await ctx.send( + f"{Emojis.defcon_updated} DEFCON days updated; accounts must be {days} days old to join to the server" + ) + + await self.modlog.send_log_message( + Icons.defcon_updated, Colour.blurple(), "DEFCON updated", + f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" + f"**Days:** {self.days.days}" + ) def setup(bot: Bot): diff --git a/bot/cogs/deployment.py b/bot/cogs/deployment.py index ca42fd980..790af582b 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,14 @@ class Deployment: def __init__(self, bot: Bot): self.bot = bot - @command(name="redeploy()", aliases=["bot.redeploy", "bot.redeploy()", "redeploy"]) + @group(name='redeploy') + @with_role(Roles.owner, Roles.admin, Roles.moderator) + 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 +39,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 +56,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..30e528efa 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,12 @@ 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',)) + @with_role(Roles.owner, Roles.admin) + 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..f089e0b5a 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,14 @@ 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')) @with_role(Roles.owner, Roles.admin, Roles.moderator) - async def otname_add(self, ctx, name: OffTopicName): + 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 add_command(self, ctx, name: OffTopicName): """Adds a new off-topic name to the rotation.""" result = await self.bot.http_session.post( @@ -106,9 +111,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..17acf757b 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: @@ -82,7 +79,18 @@ class Snekbox: log.info(f"Received code from {ctx.author.name}#{ctx.author.discriminator} for evaluation:\n{code}") self.jobs[ctx.author.id] = datetime.datetime.now() - code = [f" {line}" for line in code.split("\n")] + while code.startswith("\n"): + code = code[1:] + + if code.startswith("```") and code.endswith("```"): + code = code[3:-3] + + if code.startswith("python"): + code = code[6:] + elif code.startswith("py"): + code = code[2:] + + code = [f" {line.strip()}" for line in code.split("\n")] code = CODE_TEMPLATE.replace("{CODE}", "\n".join(code)) try: @@ -159,7 +167,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..7499b2b1c 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, *, tag_name: TagNameConverter=None): + """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, tag_name=tag_name) - @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..22e0cfbe7 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. """ @@ -85,4 +85,4 @@ class Utils: def setup(bot): bot.add_cog(Utils(bot)) - log.info("Utils cog loaded") + log.info("Cog loaded: Utils") 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/constants.py b/bot/constants.py index 4a3b4f133..205b09111 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -202,6 +202,10 @@ class Emojis(metaclass=YAMLGetter): section = "bot" subsection = "emojis" + defcon_disabled: str # noqa: E704 + defcon_enabled: str # noqa: E704 + defcon_updated: str # noqa: E704 + green_chevron: str red_chevron: str white_chevron: str @@ -218,6 +222,11 @@ class Icons(metaclass=YAMLGetter): crown_green: str crown_red: str + defcon_denied: str # noqa: E704 + defcon_disabled: str # noqa: E704 + defcon_enabled: str # noqa: E704 + defcon_updated: str # noqa: E704 + guild_update: str hash_blurple: str 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 diff --git a/config-default.yml b/config-default.yml index dc193f149..50505d4da 100644 --- a/config-default.yml +++ b/config-default.yml @@ -7,6 +7,10 @@ bot: tags: 60 emojis: + defcon_disabled: "<:defcondisabled:470326273952972810>" + defcon_enabled: "<:defconenabled:470326274213150730>" + defcon_updated: "<:defconsettingsupdated:470326274082996224>" + green_chevron: "<:greenchevron:418104310329769993>" red_chevron: "<:redchevron:418112778184818698>" white_chevron: "<:whitechevron:418110396973711363>" @@ -20,6 +24,11 @@ bot: crown_green: "https://cdn.discordapp.com/emojis/469964154719961088.png" crown_red: "https://cdn.discordapp.com/emojis/469964154879344640.png" + defcon_denied: "https://cdn.discordapp.com/emojis/472475292078964738.png" + defcon_disabled: "https://cdn.discordapp.com/emojis/470326273952972810.png" + defcon_enabled: "https://cdn.discordapp.com/emojis/470326274213150730.png" + defcon_updated: "https://cdn.discordapp.com/emojis/472472638342561793.png" + guild_update: "https://cdn.discordapp.com/emojis/469954765141442561.png" hash_blurple: "https://cdn.discordapp.com/emojis/469950142942806017.png" @@ -27,8 +36,8 @@ bot: hash_red: "https://cdn.discordapp.com/emojis/469950145413251072.png" message_bulk_delete: "https://cdn.discordapp.com/emojis/469952898994929668.png" - message_delete: "https://cdn.discordapp.com/emojis/469952898516779008.png" - message_edit: "https://cdn.discordapp.com/emojis/469952898143485972.png" + message_delete: "https://cdn.discordapp.com/emojis/472472641320648704.png" + message_edit: "https://cdn.discordapp.com/emojis/472472638976163870.png" sign_in: "https://cdn.discordapp.com/emojis/469952898181234698.png" sign_out: "https://cdn.discordapp.com/emojis/469952898089091082.png" |