aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/__init__.py218
-rw-r--r--bot/__main__.py14
-rw-r--r--bot/cogs/bigbrother.py13
-rw-r--r--bot/cogs/bot.py25
-rw-r--r--bot/cogs/cogs.py21
-rw-r--r--bot/cogs/defcon.py138
-rw-r--r--bot/cogs/deployment.py19
-rw-r--r--bot/cogs/doc.py30
-rw-r--r--bot/cogs/eval.py9
-rw-r--r--bot/cogs/events.py4
-rw-r--r--bot/cogs/hiphopify.py6
-rw-r--r--bot/cogs/off_topic_names.py15
-rw-r--r--bot/cogs/snakes.py80
-rw-r--r--bot/cogs/snekbox.py22
-rw-r--r--bot/cogs/tags.py48
-rw-r--r--bot/cogs/utils.py6
-rw-r--r--bot/cogs/verification.py24
-rw-r--r--bot/constants.py9
-rw-r--r--bot/formatter.py152
-rw-r--r--config-default.yml13
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"