aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Leon Sandøy <[email protected]>2018-03-14 21:21:02 +0100
committerGravatar Jeremiah Boby <[email protected]>2018-03-14 20:21:02 +0000
commitbe287bd17e5aa22b937dcfbc9b3d2b4a90ab87ef (patch)
tree428e1cf6f7234dabcd4d0c2064d0889f54eb6e2a
parentLogging fixes (diff)
Logging cogs (#32)
* added logging to events and verification cogs * Added logging to the tags cog * Added logging to Fun, Logging, and Security cogs. * Added logging to Deployment cog. * Added logging to cog management cog * Added logging to clickup cog * Changed some logging levels across all cogs. Most of my log.infos should have been log.debugs. Thanks Joseph. * Added logging to the Bot cog. * Adding logging to the non-cog files. * Added the logging handler to Eval, but didn't add the actual Logging. I'll leave that to Martmist * A couple of minor bugfixes * Add "trace" logging level * Add logging for paginator * Add JSON logging for future datadog integration * Changed some loglevels to trace now that we have the trace method, and also fixed stuff pointed out by joseph in his review. * Completed testing for bot.py * Completed testing for clickup.py * Completed testing for cogs.py * Completed testing for deployment.py * Completed testing for fun.py * Finished logging for bot and tags.py, as well as __init__.py * Addressing all feedback in request for changes.
-rw-r--r--.gitignore3
-rw-r--r--bot/__init__.py83
-rw-r--r--bot/cogs/bot.py87
-rw-r--r--bot/cogs/clickup.py60
-rw-r--r--bot/cogs/cogs.py178
-rw-r--r--bot/cogs/deployment.py16
-rw-r--r--bot/cogs/eval.py7
-rw-r--r--bot/cogs/events.py25
-rw-r--r--bot/cogs/fun.py13
-rw-r--r--bot/cogs/logging.py8
-rw-r--r--bot/cogs/security.py6
-rw-r--r--bot/cogs/tags.py82
-rw-r--r--bot/cogs/verification.py18
-rw-r--r--bot/constants.py1
-rw-r--r--bot/decorators.py22
-rw-r--r--bot/formatter.py7
-rw-r--r--bot/pagination.py35
-rw-r--r--requirements.txt1
-rw-r--r--scripts/vagrant_bootstrap.sh47
19 files changed, 523 insertions, 176 deletions
diff --git a/.gitignore b/.gitignore
index 2497f6deb..f8625a961 100644
--- a/.gitignore
+++ b/.gitignore
@@ -105,3 +105,6 @@ ENV/
# Vagrant
.vagrant
+
+# JSON logfile
+log.json \ No newline at end of file
diff --git a/bot/__init__.py b/bot/__init__.py
index c8220a6b3..289564ee7 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -2,28 +2,65 @@
import ast
import logging
import sys
-from logging import StreamHandler
+from logging import Logger, StreamHandler
from logging.handlers import SysLogHandler
import discord.ext.commands.view
+from logmatic import JsonFormatter
+
from bot.constants import PAPERTRAIL_ADDRESS, PAPERTRAIL_PORT
+logging.TRACE = 5
+logging.addLevelName(logging.TRACE, "TRACE")
+
+
+def monkeypatch_trace(self, msg, *args, **kwargs):
+ """
+ Log 'msg % args' with severity 'TRACE'.
+
+ To pass exception information, use the keyword argument exc_info with
+ a true value, e.g.
+
+ logger.trace("Houston, we have an %s", "interesting problem", exc_info=1)
+ """
+ if self.isEnabledFor(logging.TRACE):
+ self._log(logging.TRACE, msg, args, **kwargs)
+
+
+Logger.trace = monkeypatch_trace
+
+# Set up logging
logging_handlers = []
if PAPERTRAIL_ADDRESS:
- logging_handlers.append(SysLogHandler(address=(PAPERTRAIL_ADDRESS, PAPERTRAIL_PORT)))
+ papertrail_handler = SysLogHandler(address=(PAPERTRAIL_ADDRESS, PAPERTRAIL_PORT))
+ papertrail_handler.setLevel(logging.DEBUG)
+ logging_handlers.append(papertrail_handler)
logging_handlers.append(StreamHandler(stream=sys.stderr))
+json_handler = logging.FileHandler(filename="log.json", mode="w")
+json_handler.formatter = JsonFormatter()
+logging_handlers.append(json_handler)
+
logging.basicConfig(
format="%(asctime)s pd.beardfist.com Bot: | %(name)30s | %(levelname)8s | %(message)s",
datefmt="%b %d %H:%M:%S",
- level=logging.INFO,
+ level=logging.TRACE,
handlers=logging_handlers
)
+log = logging.getLogger(__name__)
+
+# Silence discord and websockets
+logging.getLogger("discord.client").setLevel(logging.ERROR)
+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:
"""
@@ -69,10 +106,35 @@ def _get_word(self) -> str:
self.previous = self.index
result = self.buffer[self.index:self.index + pos]
self.index += pos
-
- if current == "(" and self.buffer[self.index + 1] != ")":
+ next = None
+
+ # Check what's after the '('
+ if len(self.buffer) != self.index:
+ next = self.buffer[self.index + 1]
+
+ # Is it possible to parse this without syntax error?
+ syntax_valid = True
+ try:
+ ast.literal_eval(self.buffer[self.index:])
+ except SyntaxError:
+ log.warning("The command cannot be parsed by ast.literal_eval because it raises a SyntaxError.")
+ # TODO: It would be nice if this actually made the bot return a SyntaxError. ClickUp #1b12z # noqa: T000
+ syntax_valid = False
+
+ # Conditions for a valid, parsable command.
+ python_parse_conditions = (
+ current == "("
+ and next
+ and next != ")"
+ and syntax_valid
+ )
+
+ if python_parse_conditions:
+ log.debug(f"A python-style command was used. Attempting to parse. Buffer is {self.buffer}. "
+ "A step-by-step can be found in the trace log.")
# Parse the args
+ log.trace("Parsing command with ast.literal_eval.")
args = self.buffer[self.index:]
args = ast.literal_eval(args)
@@ -86,17 +148,24 @@ def _get_word(self) -> str:
# 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)
# Adding double quotes to every argument
+ log.trace(f"Wrapping all args in double quotes.")
new_args.append(f'"{arg}"')
+ # Add the result to the buffer
new_args = " ".join(new_args)
self.buffer = f"{self.buffer[:self.index]} {new_args}"
- self.end = len(self.buffer) # Recalibrate the end since we've removed commas
+ log.trace(f"Modified the buffer. New buffer is now {self.buffer}")
+
+ # Recalibrate the end since we've removed commas
+ self.end = len(self.buffer)
- else:
+ elif current == "(" and next == ")":
# Move the cursor to capture the ()'s
+ log.debug("User called command without providing arguments.")
pos += 2
result = self.buffer[self.previous:self.index + (pos+2)]
self.index += 2
diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py
index 7c34e1e7c..600285cf6 100644
--- a/bot/cogs/bot.py
+++ b/bot/cogs/bot.py
@@ -1,9 +1,10 @@
# coding=utf-8
-# import ast
+import ast
+import logging
import re
-# import time
+import time
-from discord import Embed # Message
+from discord import Embed, Message
from discord.ext.commands import AutoShardedBot, Context, command, group
from dulwich.repo import Repo
@@ -13,6 +14,8 @@ from bot.constants import (BOT_CHANNEL, DEVTEST_CHANNEL, HELP1_CHANNEL,
PYTHON_GUILD, VERIFIED_ROLE)
from bot.decorators import with_role
+log = logging.getLogger(__name__)
+
class Bot:
"""
@@ -64,6 +67,7 @@ class Bot:
icon_url="https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle.png"
)
+ log.info(f"{ctx.author} called bot.about(). Returning information about the bot.")
await ctx.send(embed=embed)
@command(name="info()", aliases=["bot.info", "bot.about", "bot.about()", "info", "bot.info()"])
@@ -85,9 +89,12 @@ class Bot:
if msg.count("\n") >= 3:
# Filtering valid Python codeblocks and exiting if a valid Python codeblock is found
if re.search("```(python|py)\n((?:.*\n*)+)```", msg, re.IGNORECASE):
+ log.trace("Someone wrote a message that was already a "
+ "valid Python syntax highlighted code block. No action taken.")
return None
else:
# Stripping backticks from every line of the message.
+ log.trace(f"Stripping backticks from message.\n\n{msg}\n\n")
content = ""
for line in msg.splitlines():
content += line.strip("`") + "\n"
@@ -95,6 +102,7 @@ class Bot:
content = content.strip()
# Remove "Python" or "Py" from top of the message if exists
+ log.trace(f"Removing 'py' or 'python' from message.\n\n{content}\n\n")
if content.lower().startswith("python"):
content = content[6:]
elif content.lower().startswith("py"):
@@ -103,41 +111,50 @@ class Bot:
# Strip again to remove the whitespace(s) left before the code
# If the msg looked like "Python <code>" before removing Python
content = content.strip()
+ log.trace(f"Returning message.\n\n{content}\n\n")
return content
-# async def on_message(self, msg: Message):
-# if msg.channel.id in self.channel_cooldowns:
-# on_cooldown = time.time() - self.channel_cooldowns[msg.channel.id] < 300
-# if not on_cooldown or msg.channel.id == DEVTEST_CHANNEL:
-# try:
-# content = self.codeblock_stripping(msg.content)
-# if not content:
-# return
-#
-# # Attempts to parse the message into an AST node.
-# # Invalid Python code will raise a SyntaxError.
-# tree = ast.parse(content)
-#
-# # Multiple lines of single words could be interpreted as expressions.
-# # This check is to avoid all nodes being parsed as expressions.
-# # (e.g. words over multiple lines)
-# if not all(isinstance(node, ast.Expr) for node in tree.body):
-# codeblock_tag = await self.bot.get_cog("Tags").get_tag_data("codeblock")
-# if codeblock_tag == {}:
-# # todo: add logging
-# return
-# howto = (f"Hey {msg.author.mention}!\n\n"
-# "I noticed you were trying to paste code into this channel.\n\n"
-# f"{codeblock_tag['tag_content']}")
-#
-# howto_embed = Embed(description=howto)
-# await msg.channel.send(embed=howto_embed)
-# self.channel_cooldowns[msg.channel.id] = time.time()
-# except SyntaxError:
-# # todo: add logging
-# pass
+ async def on_message(self, msg: Message):
+ if msg.channel.id in self.channel_cooldowns:
+ on_cooldown = time.time() - self.channel_cooldowns[msg.channel.id] < 300
+ if not on_cooldown or msg.channel.id == DEVTEST_CHANNEL:
+ try:
+ content = self.codeblock_stripping(msg.content)
+ if not content:
+ return
+
+ # Attempts to parse the message into an AST node.
+ # Invalid Python code will raise a SyntaxError.
+ tree = ast.parse(content)
+
+ # Multiple lines of single words could be interpreted as expressions.
+ # This check is to avoid all nodes being parsed as expressions.
+ # (e.g. words over multiple lines)
+ if not all(isinstance(node, ast.Expr) for node in tree.body):
+ codeblock_tag = await self.bot.get_cog("Tags").get_tag_data("codeblock")
+
+ if codeblock_tag == {}:
+ log.warning(f"{msg.author} posted something that needed to be put inside Python "
+ "code blocks, but the 'codeblock' tag was not in the tags database!")
+ return
+
+ log.debug(f"{msg.author} posted something that needed to be put inside python code blocks. "
+ "Sending the user some instructions.")
+ howto = (f"Hey {msg.author.mention}!\n\n"
+ "I noticed you were trying to paste code into this channel.\n\n"
+ f"{codeblock_tag['tag_content']}")
+
+ howto_embed = Embed(description=howto)
+ await msg.channel.send(embed=howto_embed)
+ self.channel_cooldowns[msg.channel.id] = time.time()
+
+ except SyntaxError:
+ log.trace(f"{msg.author} posted in a help channel, and when we tried to parse it as Python code, "
+ "ast.parse raised a SyntaxError. This probably just means it wasn't Python code. "
+ f"The message that was posted was:\n\n{msg.content}\n\n")
+ pass
def setup(bot):
bot.add_cog(Bot(bot))
- print("Cog loaded: Bot")
+ log.info("Cog loaded: Bot")
diff --git a/bot/cogs/clickup.py b/bot/cogs/clickup.py
index db94a96fa..03c1238f6 100644
--- a/bot/cogs/clickup.py
+++ b/bot/cogs/clickup.py
@@ -1,4 +1,6 @@
# coding=utf-8
+import logging
+
from discord import Colour, Embed
from discord.ext.commands import AutoShardedBot, Context, command
@@ -11,14 +13,11 @@ from bot.decorators import with_role
from bot.pagination import LinePaginator
from bot.utils import CaseInsensitiveDict
-
CREATE_TASK_URL = "https://api.clickup.com/api/v1/list/{list_id}/task"
EDIT_TASK_URL = "https://api.clickup.com/api/v1/task/{task_id}"
GET_TASKS_URL = "https://api.clickup.com/api/v1/team/{team_id}/task"
PROJECTS_URL = "https://api.clickup.com/api/v1/space/{space_id}/project"
-
-# Don't ask me why the below line is a syntax error, but that's what flake8 thinks...
-SPACES_URL = "https://api.clickup.com/api/v1/team/{team_id}/space" # flake8: noqa
+SPACES_URL = "https://api.clickup.com/api/v1/team/{team_id}/space"
TEAM_URL = "https://api.clickup.com/api/v1/team/{team_id}"
HEADERS = {
@@ -28,6 +27,8 @@ HEADERS = {
STATUSES = ["open", "in progress", "review", "closed"]
+log = logging.getLogger(__name__)
+
class ClickUp:
"""
@@ -45,7 +46,7 @@ class ClickUp:
result = await response.json()
if "err" in result:
- print(f"Failed to get ClickUp lists: `{result['ECODE']}`: {result['err']}")
+ log.error(f"Failed to get ClickUp lists: `{result['ECODE']}`: {result['err']}")
else:
# Save all the lists with their IDs so that we can get at them later
for project in result["projects"]:
@@ -81,8 +82,9 @@ class ClickUp:
if task_list in self.lists:
params["list_ids[]"] = self.lists[task_list]
else:
- embed.colour = Colour.red()
+ log.warning(f"{ctx.author} requested '{task_list}', but that list is unknown. Rejecting request.")
embed.description = f"Unknown list: {task_list}"
+ embed.colour = Colour.red()
return await ctx.send(embed=embed)
if status and status != "*":
@@ -94,6 +96,9 @@ class ClickUp:
result = await response.json()
if "err" in result:
+ log.error("ClickUp responded to the task list request with an error!\n"
+ f"error code: '{result['ECODE']}'\n"
+ f"error: {result['err']}")
embed.description = f"`{result['ECODE']}`: {result['err']}"
embed.colour = Colour.red()
@@ -101,8 +106,10 @@ class ClickUp:
tasks = result["tasks"]
if not tasks:
- embed.colour = Colour.red()
+ log.debug(f"{ctx.author} requested a list of ClickUp tasks, but no ClickUp tasks were found.")
embed.description = "No tasks found."
+ embed.colour = Colour.red()
+
else:
lines = []
@@ -112,6 +119,8 @@ class ClickUp:
status = f"{task['status']['status'].title()}"
lines.append(f"{id_fragment} ({status})\n\u00BB {task['name']}")
+
+ log.debug(f"{ctx.author} requested a list of ClickUp tasks. Returning list.")
return await LinePaginator.paginate(lines, ctx, embed, max_size=750)
return await ctx.send(embed=embed)
@@ -141,6 +150,9 @@ class ClickUp:
result = await response.json()
if "err" in result:
+ log.error("ClickUp responded to the get task request with an error!\n"
+ f"error code: '{result['ECODE']}'\n"
+ f"error: {result['err']}")
embed.description = f"`{result['ECODE']}`: {result['err']}"
embed.colour = Colour.red()
else:
@@ -152,6 +164,7 @@ class ClickUp:
break
if task is None:
+ log.warning(f"{ctx.author} requested the task '#{task_id}', but it could not be found.")
embed.description = f"Unable to find task with ID `#{task_id}`:"
embed.colour = Colour.red()
else:
@@ -175,6 +188,7 @@ class ClickUp:
f"**Assignees**\n{assignees}"
)
+ log.debug(f"{ctx.author} requested the task '#{task_id}'. Returning the task data.")
return await LinePaginator.paginate(lines, ctx, embed, max_size=750)
return await ctx.send(embed=embed)
@@ -191,11 +205,15 @@ class ClickUp:
result = await response.json()
if "err" in result:
+ log.error("ClickUp responded to the team request with an error!\n"
+ f"error code: '{result['ECODE']}'\n"
+ f"error: {result['err']}")
embed = Embed(
colour=Colour.red(),
description=f"`{result['ECODE']}`: {result['err']}"
)
else:
+ log.debug(f"{ctx.author} requested a list of team members. Preparing the list...")
embed = Embed(
colour=Colour.blurple()
)
@@ -212,6 +230,7 @@ class ClickUp:
url=f"https://app.clickup.com/{CLICKUP_TEAM}/{CLICKUP_SPACE}/"
)
+ log.debug("List fully prepared, returning list to channel.")
await ctx.send(embed=embed)
@command(name="clickup.lists()", aliases=["clickup.lists", "lists"])
@@ -227,11 +246,15 @@ class ClickUp:
result = await response.json()
if "err" in result:
+ log.error("ClickUp responded to the lists request with an error!\n"
+ f"error code: '{result['ECODE']}'\n"
+ f"error: {result['err']}")
embed = Embed(
colour=Colour.red(),
description=f"`{result['ECODE']}`: {result['err']}"
)
else:
+ log.debug(f"{ctx.author} requested a list of all ClickUp lists. Preparing the list...")
embed = Embed(
colour=Colour.blurple()
)
@@ -255,11 +278,12 @@ class ClickUp:
url=f"https://app.clickup.com/{CLICKUP_TEAM}/{CLICKUP_SPACE}/"
)
+ log.debug(f"List fully prepared, returning list to channel.")
await ctx.send(embed=embed)
@command(name="clickup.open()", aliases=["clickup.open", "open", "open_task"])
@with_role(MODERATOR_ROLE, ADMIN_ROLE, OWNER_ROLE, DEVOPS_ROLE)
- async def open_command(self, ctx: Context, task_list: str, *, title: str):
+ async def open_command(self, ctx: Context, task_list: str, title: str):
"""
Open a new task under a specific task list, with a title
@@ -277,8 +301,10 @@ class ClickUp:
if task_list in self.lists:
task_list = self.lists[task_list]
else:
- embed.colour = Colour.red()
+ log.warning(f"{ctx.author} tried to open a new task on ClickUp, "
+ f"but '{task_list}' is not a known list. Rejecting request.")
embed.description = f"Unknown list: {task_list}"
+ embed.colour = Colour.red()
return await ctx.send(embed=embed)
response = await self.bot.http_session.post(
@@ -290,6 +316,9 @@ class ClickUp:
result = await response.json()
if "err" in result:
+ log.error("ClickUp responded to the get task request with an error!\n"
+ f"error code: '{result['ECODE']}'\n"
+ f"error: {result['err']}")
embed.colour = Colour.red()
embed.description = f"`{result['ECODE']}`: {result['err']}"
else:
@@ -298,13 +327,15 @@ class ClickUp:
project, task_list = self.lists[task_list].split("/", 1)
task_list = f"{project.title()}/{task_list.title()}"
+ log.debug(f"{ctx.author} opened a new task on ClickUp: \n"
+ f"{task_list} - #{task_id}")
embed.description = f"New task created: [{task_list} \u00BB `#{task_id}`]({task_url})"
await ctx.send(embed=embed)
@command(name="clickup.set_status()", aliases=["clickup.set_status", "set_status", "set_task_status"])
@with_role(MODERATOR_ROLE, ADMIN_ROLE, OWNER_ROLE, DEVOPS_ROLE)
- async def set_status_command(self, ctx: Context, task_id: str, *, status: str):
+ async def set_status_command(self, ctx: Context, task_id: str, status: str):
"""
Update the status of a specific task
"""
@@ -317,8 +348,9 @@ class ClickUp:
)
if status.lower() not in STATUSES:
- embed.colour = Colour.red()
+ log.warning(f"{ctx.author} tried to update a task on ClickUp, but '{status}' is not a known status.")
embed.description = f"Unknown status: {status}"
+ embed.colour = Colour.red()
else:
response = await self.bot.http_session.put(
EDIT_TASK_URL.format(task_id=task_id), headers=HEADERS, json={"status": status}
@@ -326,9 +358,13 @@ class ClickUp:
result = await response.json()
if "err" in result:
+ log.error("ClickUp responded to the get task request with an error!\n"
+ f"error code: '{result['ECODE']}'\n"
+ f"error: {result['err']}")
embed.description = f"`{result['ECODE']}`: {result['err']}"
embed.colour = Colour.red()
else:
+ log.debug(f"{ctx.author} updated a task on ClickUp: #{task_id}")
task_url = f"https://app.clickup.com/{CLICKUP_TEAM}/{CLICKUP_SPACE}/t/{task_id}"
embed.description = f"Task updated: [`#{task_id}`]({task_url})"
@@ -337,4 +373,4 @@ class ClickUp:
def setup(bot):
bot.add_cog(ClickUp(bot))
- print("Cog loaded: ClickUp")
+ log.info("Cog loaded: ClickUp")
diff --git a/bot/cogs/cogs.py b/bot/cogs/cogs.py
index 774f5a68d..0079b5e93 100644
--- a/bot/cogs/cogs.py
+++ b/bot/cogs/cogs.py
@@ -1,4 +1,5 @@
# coding=utf-8
+import logging
import os
from discord import ClientException, Colour, Embed
@@ -12,6 +13,8 @@ from bot.constants import (
from bot.decorators import with_role
from bot.pagination import LinePaginator
+log = logging.getLogger(__name__)
+
class Cogs:
"""
@@ -23,6 +26,7 @@ class Cogs:
self.cogs = {}
# Load up the cog names
+ log.info("Initializing cog names...")
for filename in os.listdir("bot/cogs"):
if filename.endswith(".py") and "_" not in filename:
if os.path.isfile(f"bot/cogs/{filename}"):
@@ -60,22 +64,33 @@ class Cogs:
full_cog = cog
else:
full_cog = None
+ log.warning(f"{ctx.author} requested we load the '{cog}' cog, but that cog doesn't exist.")
embed.description = f"Unknown cog: {cog}"
- if full_cog not in self.bot.extensions:
- try:
- self.bot.load_extension(full_cog)
- except ClientException:
- embed.description = f"Invalid cog: {cog}\n\nCog does not have a `setup()` function"
- except ImportError:
- embed.description = f"Invalid cog: {cog}\n\nCould not find cog module {full_cog}"
- except Exception as e:
- embed.description = f"Failed to load cog: {cog}\n\n```{e}```"
+ if full_cog:
+ if full_cog not in self.bot.extensions:
+ try:
+ self.bot.load_extension(full_cog)
+ except ClientException:
+ log.error(f"{ctx.author} requested we load the '{cog}' cog, "
+ "but that cog doesn't have a 'setup()' function.")
+ embed.description = f"Invalid cog: {cog}\n\nCog does not have a `setup()` function"
+ except ImportError:
+ log.error(f"{ctx.author} requested we load the '{cog}' cog, "
+ f"but the cog module {full_cog} could not be found!")
+ embed.description = f"Invalid cog: {cog}\n\nCould not find cog module {full_cog}"
+ except Exception as e:
+ log.error(f"{ctx.author} requested we load the '{cog}' cog, "
+ "but the loading failed with the following error: \n"
+ f"{e}")
+ embed.description = f"Failed to load cog: {cog}\n\n```{e}```"
+ else:
+ log.debug(f"{ctx.author} requested we load the '{cog}' cog. Cog loaded!")
+ embed.description = f"Cog loaded: {cog}"
+ embed.colour = Colour.green()
else:
- embed.description = f"Cog loaded: {cog}"
- embed.colour = Colour.green()
- else:
- embed.description = f"Cog {cog} is already loaded"
+ log.warning(f"{ctx.author} requested we load the '{cog}' cog, but the cog was already loaded!")
+ embed.description = f"Cog {cog} is already loaded"
await ctx.send(embed=embed)
@@ -106,20 +121,28 @@ class Cogs:
full_cog = cog
else:
full_cog = None
+ log.warning(f"{ctx.author} requested we unload the '{cog}' cog, but that cog doesn't exist.")
embed.description = f"Unknown cog: {cog}"
- if full_cog == "bot.cogs.cogs":
- embed.description = "You may not unload the cog management cog!"
- elif full_cog in self.bot.extensions:
- try:
- self.bot.unload_extension(full_cog)
- except Exception as e:
- embed.description = f"Failed to unload cog: {cog}\n\n```{e}```"
+ if full_cog:
+ if full_cog == "bot.cogs.cogs":
+ log.warning(f"{ctx.author} requested we unload the cog management cog, that sneaky pete. We said no.")
+ embed.description = "You may not unload the cog management cog!"
+ elif full_cog in self.bot.extensions:
+ try:
+ self.bot.unload_extension(full_cog)
+ except Exception as e:
+ log.error(f"{ctx.author} requested we unload the '{cog}' cog, "
+ "but the unloading failed with the following error: \n"
+ f"{e}")
+ embed.description = f"Failed to unload cog: {cog}\n\n```{e}```"
+ else:
+ log.debug(f"{ctx.author} requested we unload the '{cog}' cog. Cog unloaded!")
+ embed.description = f"Cog unloaded: {cog}"
+ embed.colour = Colour.green()
else:
- embed.description = f"Cog unloaded: {cog}"
- embed.colour = Colour.green()
- else:
- embed.description = f"Cog {cog} is not loaded"
+ log.warning(f"{ctx.author} requested we unload the '{cog}' cog, but the cog wasn't loaded!")
+ embed.description = f"Cog {cog} is not loaded"
await ctx.send(embed=embed)
@@ -155,70 +178,80 @@ class Cogs:
full_cog = cog
else:
full_cog = None
+ log.warning(f"{ctx.author} requested we reload the '{cog}' cog, but that cog doesn't exist.")
embed.description = f"Unknown cog: {cog}"
- if full_cog == "*":
- all_cogs = [
- f"bot.cogs.{fn[:-3]}" for fn in os.listdir("bot/cogs")
- if os.path.isfile(f"bot/cogs/{fn}") and fn.endswith(".py") and "_" not in fn
- ]
+ if full_cog:
+ if full_cog == "*":
+ all_cogs = [
+ f"bot.cogs.{fn[:-3]}" for fn in os.listdir("bot/cogs")
+ if os.path.isfile(f"bot/cogs/{fn}") and fn.endswith(".py") and "_" not in fn
+ ]
- failed_unloads = {}
- failed_loads = {}
+ failed_unloads = {}
+ failed_loads = {}
- unloaded = 0
- loaded = 0
+ unloaded = 0
+ loaded = 0
- for loaded_cog in self.bot.extensions.copy().keys():
- try:
- self.bot.unload_extension(loaded_cog)
- except Exception as e:
- failed_unloads[loaded_cog] = str(e)
- else:
- unloaded += 1
+ for loaded_cog in self.bot.extensions.copy().keys():
+ try:
+ self.bot.unload_extension(loaded_cog)
+ except Exception as e:
+ failed_unloads[loaded_cog] = str(e)
+ else:
+ unloaded += 1
- for unloaded_cog in all_cogs:
- try:
- self.bot.load_extension(unloaded_cog)
- except Exception as e:
- failed_loads[unloaded_cog] = str(e)
- else:
- loaded += 1
+ for unloaded_cog in all_cogs:
+ try:
+ self.bot.load_extension(unloaded_cog)
+ except Exception as e:
+ failed_loads[unloaded_cog] = str(e)
+ else:
+ loaded += 1
- lines = [
- "**All cogs reloaded**",
- f"**Unloaded**: {unloaded} / **Loaded**: {loaded}"
- ]
+ lines = [
+ "**All cogs reloaded**",
+ f"**Unloaded**: {unloaded} / **Loaded**: {loaded}"
+ ]
- if failed_unloads:
- lines.append("\n**Unload failures**")
+ if failed_unloads:
+ lines.append("\n**Unload failures**")
- for cog, error in failed_unloads:
- lines.append(f"`{cog}` {WHITE_CHEVRON} `{error}`")
+ for cog, error in failed_unloads:
+ lines.append(f"`{cog}` {WHITE_CHEVRON} `{error}`")
- if failed_loads:
- lines.append("\n**Load failures**")
+ if failed_loads:
+ lines.append("\n**Load failures**")
- for cog, error in failed_loads:
- lines.append(f"`{cog}` {WHITE_CHEVRON} `{error}`")
+ for cog, error in failed_loads:
+ lines.append(f"`{cog}` {WHITE_CHEVRON} `{error}`")
- return await LinePaginator.paginate(lines, ctx, embed, empty=False)
+ log.debug(f"{ctx.author} requested we reload all cogs. Here are the results: \n"
+ f"{lines}")
- elif full_cog in self.bot.extensions:
- try:
- self.bot.unload_extension(full_cog)
- self.bot.load_extension(full_cog)
- except Exception as e:
- embed.description = f"Failed to reload cog: {cog}\n\n```{e}```"
+ return await LinePaginator.paginate(lines, ctx, embed, empty=False)
+
+ elif full_cog in self.bot.extensions:
+ try:
+ self.bot.unload_extension(full_cog)
+ self.bot.load_extension(full_cog)
+ except Exception as e:
+ log.error(f"{ctx.author} requested we reload the '{cog}' cog, "
+ "but the unloading failed with the following error: \n"
+ f"{e}")
+ embed.description = f"Failed to reload cog: {cog}\n\n```{e}```"
+ else:
+ log.debug(f"{ctx.author} requested we reload the '{cog}' cog. Cog reloaded!")
+ embed.description = f"Cog reload: {cog}"
+ embed.colour = Colour.green()
else:
- embed.description = f"Cog reload: {cog}"
- embed.colour = Colour.green()
- else:
- embed.description = f"Cog {cog} is not loaded"
+ log.warning(f"{ctx.author} requested we reload the '{cog}' cog, but the cog wasn't loaded!")
+ embed.description = f"Cog {cog} is not loaded"
await ctx.send(embed=embed)
- @command(name="cogs.get_all()", aliases=["cogs.get_all", "get_cogs", "get_all_cogs", "cogs", "cogs.list"])
+ @command(name="cogs.list()", aliases=["cogs", "cogs.list", "cogs()"])
@with_role(MODERATOR_ROLE, ADMIN_ROLE, OWNER_ROLE, DEVOPS_ROLE)
async def list_command(self, ctx: Context):
"""
@@ -262,9 +295,10 @@ class Cogs:
lines.append(f"{chevron} {cog}")
+ log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.")
await LinePaginator.paginate(lines, ctx, embed, max_size=300, empty=False)
def setup(bot):
bot.add_cog(Cogs(bot))
- print("Cog loaded: Cogs")
+ log.info("Cog loaded: Cogs")
diff --git a/bot/cogs/deployment.py b/bot/cogs/deployment.py
index dcc478917..b85d01ad2 100644
--- a/bot/cogs/deployment.py
+++ b/bot/cogs/deployment.py
@@ -1,10 +1,14 @@
# coding=utf-8
+import logging
+
from discord import Colour, Embed
from discord.ext.commands import AutoShardedBot, Context, command
from bot.constants import ADMIN_ROLE, DEPLOY_BOT_KEY, DEPLOY_SITE_KEY, DEPLOY_URL, DEVOPS_ROLE, OWNER_ROLE, STATUS_URL
from bot.decorators import with_role
+log = logging.getLogger(__name__)
+
class Deployment:
"""
@@ -22,11 +26,13 @@ class Deployment:
"""
response = await self.bot.http_session.get(DEPLOY_URL, headers={"token": DEPLOY_BOT_KEY})
- result = response.text()
+ result = await response.text()
if result == "True":
+ log.debug(f"{ctx.author} triggered deployment for bot. Deployment was started.")
await ctx.send(f"{ctx.author.mention} Bot deployment started.")
else:
+ 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"])
@@ -37,11 +43,13 @@ class Deployment:
"""
response = await self.bot.http_session.get(DEPLOY_URL, headers={"token": DEPLOY_SITE_KEY})
- result = response.text()
+ result = await response.text()
if result == "True":
+ log.debug(f"{ctx.author} triggered deployment for site. Deployment was started.")
await ctx.send(f"{ctx.author.mention} Site deployment started.")
else:
+ 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"])
@@ -51,6 +59,7 @@ class Deployment:
Check the various deployment uptimes for each service
"""
+ log.debug(f"{ctx.author} requested service uptimes.")
response = await self.bot.http_session.get(STATUS_URL)
data = await response.json()
@@ -66,9 +75,10 @@ class Deployment:
name=key, value=value, inline=True
)
+ log.debug("Uptimes retrieved and parsed, returning data.")
await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(Deployment(bot))
- print("Cog loaded: Deployment")
+ log.info("Cog loaded: Deployment")
diff --git a/bot/cogs/eval.py b/bot/cogs/eval.py
index 5a321c2c4..c37c1f449 100644
--- a/bot/cogs/eval.py
+++ b/bot/cogs/eval.py
@@ -2,6 +2,7 @@
import contextlib
import inspect
+import logging
import pprint
import re
import textwrap
@@ -15,6 +16,8 @@ from bot.constants import OWNER_ROLE
from bot.decorators import with_role
from bot.interpreter import Interpreter
+log = logging.getLogger(__name__)
+
class EvalCog: # Named this way because a flake8 plugin isn't case-sensitive
"""
@@ -148,7 +151,7 @@ class EvalCog: # Named this way because a flake8 plugin isn't case-sensitive
self.env.update(env)
- # Ignore this shitcode, it works
+ # Ignore this code, it works
_code = """
async def func(): # (None,) -> Any
try:
@@ -192,4 +195,4 @@ async def func(): # (None,) -> Any
def setup(bot):
bot.add_cog(EvalCog(bot))
- print("Cog loaded: Eval")
+ log.info("Cog loaded: Eval")
diff --git a/bot/cogs/events.py b/bot/cogs/events.py
index 244ddf3c7..a07f01f24 100644
--- a/bot/cogs/events.py
+++ b/bot/cogs/events.py
@@ -1,4 +1,6 @@
# coding=utf-8
+import logging
+
from discord import Embed, Member
from discord.ext.commands import (
AutoShardedBot, BadArgument, BotMissingPermissions,
@@ -10,6 +12,8 @@ from bot.constants import (
ADMIN_ROLE, DEVLOG_CHANNEL, DEVOPS_ROLE, MODERATOR_ROLE, OWNER_ROLE, PYTHON_GUILD, SITE_API_KEY, SITE_API_USER_URL
)
+log = logging.getLogger(__name__)
+
class Events:
"""
@@ -29,7 +33,7 @@ class Events:
return await response.json()
except Exception as e:
- print(f"Failed to send role updates: {e}")
+ log.error(f"Failed to send role updates: {e}")
return {}
async def on_command_error(self, ctx: Context, e: CommandError):
@@ -63,7 +67,7 @@ class Events:
f"Sorry, an unexpected error occurred. Please let us know!\n\n```{e}```"
)
raise e.original
- print(e)
+ log.error(f"COMMAND ERROR: '{e}'")
async def on_ready(self):
users = []
@@ -93,6 +97,7 @@ class Events:
})
if users:
+ log.debug(f"{len(users)} user roles updated")
data = await self.send_updated_users(*users) # type: dict
if any(data.values()):
@@ -114,24 +119,28 @@ class Events:
if before.roles == after.roles:
return
- roles = [r.id for r in after.roles] # type: List[int]
+ before_role_names = [role.name for role in before.roles] # type: List[str]
+ after_role_names = [role.name for role in after.roles] # type: List[str]
+ role_ids = [r.id for r in after.roles] # type: List[int]
+
+ log.debug(f"{before.display_name} roles changing from {before_role_names} to {after_role_names}")
- if OWNER_ROLE in roles:
+ if OWNER_ROLE in role_ids:
self.send_updated_users({
"user_id": after.id,
"role": OWNER_ROLE
})
- elif ADMIN_ROLE in roles:
+ elif ADMIN_ROLE in role_ids:
self.send_updated_users({
"user_id": after.id,
"role": ADMIN_ROLE
})
- elif MODERATOR_ROLE in roles:
+ elif MODERATOR_ROLE in role_ids:
self.send_updated_users({
"user_id": after.id,
"role": MODERATOR_ROLE
})
- elif DEVOPS_ROLE in roles:
+ elif DEVOPS_ROLE in role_ids:
self.send_updated_users({
"user_id": after.id,
"role": DEVOPS_ROLE
@@ -140,4 +149,4 @@ class Events:
def setup(bot):
bot.add_cog(Events(bot))
- print("Cog loaded: Events")
+ log.info("Cog loaded: Events")
diff --git a/bot/cogs/fun.py b/bot/cogs/fun.py
index 457c4b5ef..812434b12 100644
--- a/bot/cogs/fun.py
+++ b/bot/cogs/fun.py
@@ -1,4 +1,6 @@
# coding=utf-8
+import logging
+
from discord import Message
from discord.ext.commands import AutoShardedBot
@@ -6,14 +8,16 @@ from bot.constants import BOT_CHANNEL
RESPONSES = {
"_pokes {us}_": "_Pokes {them}_",
- "_eats {us}_": "_Tastes crunchy_",
+ "_eats {us}_": "_Tastes slimy and snake-like_",
"_pets {us}_": "_Purrs_"
}
+log = logging.getLogger(__name__)
+
class Fun:
"""
- Fun, mostly-useless stuff
+ Fun, entirely useless stuff
"""
def __init__(self, bot: AutoShardedBot):
@@ -41,9 +45,10 @@ class Fun:
response = RESPONSES.get(content)
if response:
- await message.channel.send(response.replace("{them}", message.author.mention))
+ log.debug(f"{message.author} said '{message.clean_content}'. Responding with '{response}'.")
+ await message.channel.send(response.format(them=message.author.mention))
def setup(bot):
bot.add_cog(Fun(bot))
- print("Cog loaded: Fun")
+ log.info("Cog loaded: Fun")
diff --git a/bot/cogs/logging.py b/bot/cogs/logging.py
index 8f66e13bb..8399e35ca 100644
--- a/bot/cogs/logging.py
+++ b/bot/cogs/logging.py
@@ -1,9 +1,13 @@
# coding=utf-8
+import logging
+
from discord import Embed
from discord.ext.commands import AutoShardedBot
from bot.constants import DEVLOG_CHANNEL
+log = logging.getLogger(__name__)
+
class Logging:
"""
@@ -14,7 +18,7 @@ class Logging:
self.bot = bot
async def on_ready(self):
- print("Connected!")
+ log.info("Bot connected!")
embed = Embed(description="Connected!")
embed.set_author(
@@ -28,4 +32,4 @@ class Logging:
def setup(bot):
bot.add_cog(Logging(bot))
- print("Cog loaded: Logging")
+ log.info("Cog loaded: Logging")
diff --git a/bot/cogs/security.py b/bot/cogs/security.py
index 421df9cfc..7b4cf3194 100644
--- a/bot/cogs/security.py
+++ b/bot/cogs/security.py
@@ -1,6 +1,10 @@
# coding=utf-8
+import logging
+
from discord.ext.commands import AutoShardedBot, Context
+log = logging.getLogger(__name__)
+
class Security:
"""
@@ -17,4 +21,4 @@ class Security:
def setup(bot):
bot.add_cog(Security(bot))
- print("Cog loaded: Security")
+ log.info("Cog loaded: Security")
diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py
index fc24cc47f..5d7d26394 100644
--- a/bot/cogs/tags.py
+++ b/bot/cogs/tags.py
@@ -1,3 +1,4 @@
+import logging
import time
from discord import Colour, Embed
@@ -8,6 +9,8 @@ from bot.constants import SITE_API_KEY, SITE_API_TAGS_URL, TAG_COOLDOWN
from bot.decorators import with_role
from bot.pagination import LinePaginator
+log = logging.getLogger(__name__)
+
class Tags:
"""
@@ -32,7 +35,7 @@ class Tags:
params = {}
if tag_name:
- params['tag_name'] = tag_name
+ params["tag_name"] = tag_name
response = await self.bot.http_session.get(SITE_API_TAGS_URL, headers=self.headers, params=params)
tag_data = await response.json()
@@ -90,6 +93,7 @@ class Tags:
:param ctx: Discord message context
"""
+ log.debug(f"{ctx.author} requested info about the tags cog")
return await ctx.invoke(self.bot.get_command("help"), "Tags")
@command(name="tags.get()", aliases=["tags.get", "tags.show()", "tags.show", "get_tag"])
@@ -128,7 +132,8 @@ class Tags:
if _command_on_cooldown(tag_name):
time_left = TAG_COOLDOWN - (time.time() - self.tag_cooldowns[tag_name]["time"])
- print(f"That command is currently on cooldown. Try again in {time_left:.1f} seconds.")
+ log.warning(f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. "
+ f"Cooldown ends in {time_left:.1f} seconds.")
return
embed = Embed()
@@ -141,6 +146,7 @@ class Tags:
embed.colour = Colour.blurple()
if tag_name:
+ log.debug(f"{ctx.author} requested the tag '{tag_name}'")
embed.title = tag_name
self.tag_cooldowns[tag_name] = {
"time": time.time(),
@@ -151,6 +157,7 @@ class Tags:
embed.title = "**Current tags**"
if isinstance(tag_data, list):
+ log.debug(f"{ctx.author} requested a list of all tags")
tags = [f"**»** {tag['tag_name']}" for tag in tag_data]
tags = sorted(tags)
@@ -160,10 +167,13 @@ class Tags:
# If not, prepare an error message.
else:
embed.colour = Colour.red()
- embed.description = "**There are no tags in the database!**"
if isinstance(tag_data, dict):
+ log.warning(f"{ctx.author} requested the tag '{tag_name}', but it could not be found.")
embed.description = f"Unknown tag: **{tag_name}**"
+ else:
+ log.warning(f"{ctx.author} requested a list of all tags, but the tags database was empty!")
+ 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().")
@@ -171,6 +181,7 @@ class Tags:
# Paginate if this is a list of all tags
if tags:
+ log.debug(f"Returning a paginated list of all tags.")
return await LinePaginator.paginate(
(lines for lines in tags),
ctx, embed,
@@ -192,38 +203,58 @@ class Tags:
:param tag_content: The content of the tag.
"""
+ def is_number(string):
+ try:
+ float(string)
+ except ValueError:
+ return False
+ else:
+ return True
+
embed = Embed()
embed.colour = Colour.red()
+ # Newline in 'tag_name'
if "\n" in tag_name:
+ log.warning(f"{ctx.author} tried to put a newline in a tag name. "
+ "Rejecting the request.")
embed.title = "Please don't do that"
embed.description = "Don't be ridiculous. Newlines are obviously not allowed in the tag name."
- elif tag_name.isdigit():
+ # 'tag_name' or 'tag_content' consists of nothing but whitespace
+ elif not tag_content.strip() or not tag_name.strip():
+ log.warning(f"{ctx.author} tried to create a tag with a name consisting only of whitespace. "
+ "Rejecting the request.")
embed.title = "Please don't do that"
- embed.description = "Tag names can't be numbers."
+ embed.description = "Tags should not be empty, or filled with whitespace."
- elif not tag_content.strip():
+ # 'tag_name' is a number of some kind, we don't allow that.
+ elif is_number(tag_name):
+ log.error("inside the is_number")
+ log.warning(f"{ctx.author} tried to create a tag with a digit as its name. "
+ "Rejecting the request.")
embed.title = "Please don't do that"
- embed.description = "Tags should not be empty, or filled with whitespace."
+ embed.description = "Tag names can't be numbers."
else:
- if not (tag_name and tag_content):
- embed.title = "Missing parameters"
- embed.description = "The tag needs both a name and some content."
- return await ctx.send(embed=embed)
-
tag_name = tag_name.lower()
tag_data = await self.post_tag_data(tag_name, tag_content)
if tag_data.get("success"):
+ log.debug(f"{ctx.author} successfully added the following tag to our database: \n"
+ f"tag_name: {tag_name}\n"
+ f"tag_content: '{tag_content}'")
embed.colour = Colour.blurple()
embed.title = "Tag successfully added"
embed.description = f"**{tag_name}** added to tag database."
else:
+ log.error("There was an unexpected database error when trying to add the following tag: \n"
+ f"tag_name: {tag_name}\n"
+ f"tag_content: '{tag_content}'\n"
+ f"response: {tag_data}")
embed.title = "Database error"
embed.description = ("There was a problem adding the data to the tags database. "
- "Please try again. If the problem persists, check the API logs.")
+ "Please try again. If the problem persists, see the error logs.")
return await ctx.send(embed=embed)
@@ -240,26 +271,31 @@ class Tags:
embed = Embed()
embed.colour = Colour.red()
- if not tag_name:
- embed.title = "Missing parameters"
- embed.description = "This method requires a `tag_name` parameter."
- return await ctx.send(embed=embed)
-
tag_data = await self.delete_tag_data(tag_name)
- if tag_data.get("success"):
+ if tag_data.get("success") is True:
+ log.debug(f"{ctx.author} successfully deleted the tag called '{tag_name}'")
embed.colour = Colour.blurple()
embed.title = tag_name
embed.description = f"Tag successfully removed: {tag_name}."
+ elif tag_data.get("success") is False:
+ log.debug(f"{ctx.author} tried to delete a tag called '{tag_name}', but the tag does not exist.")
+ embed.colour = Colour.red()
+ embed.title = tag_name
+ embed.description = "Tag doesn't appear to exist."
+
else:
- embed.title = "Database error",
- embed.description = ("There was a problem deleting the data from the tags database. "
- "Please try again. If the problem persists, check the API logs.")
+ log.error("There was an unexpected database error when trying to delete the following tag: \n"
+ f"tag_name: {tag_name}\n"
+ f"response: {tag_data}")
+ embed.title = "Database error"
+ embed.description = ("There was an unexpected error with deleting the data from the tags database. "
+ "Please try again. If the problem persists, see the error logs.")
return await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(Tags(bot))
- print("Cog loaded: Tags")
+ log.info("Cog loaded: Tags")
diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py
index 0b4935cb0..0c3fae3ed 100644
--- a/bot/cogs/verification.py
+++ b/bot/cogs/verification.py
@@ -1,16 +1,19 @@
# coding=utf-8
+import logging
+
from discord import Message, Object
from discord.ext.commands import AutoShardedBot, Context, command
from bot.constants import VERIFICATION_CHANNEL, VERIFIED_ROLE
from bot.decorators import in_channel, without_role
+log = logging.getLogger(__name__)
+
class Verification:
"""
User verification
"""
-
def __init__(self, bot: AutoShardedBot):
self.bot = bot
@@ -21,18 +24,24 @@ class Verification:
ctx = await self.bot.get_context(message) # type: Context
if ctx.command is not None and ctx.command.name == "accept":
- return # They didn't use a command, or they used a command that isn't the accept command
+ return # They used the accept command
if ctx.channel.id == VERIFICATION_CHANNEL: # We're in the verification channel
for role in ctx.author.roles:
if role.id == VERIFIED_ROLE:
+ log.warning(f"{ctx.author} posted '{ctx.message.content}' "
+ "in the verification channel, but is already verified.")
return # They're already verified
+ 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"and gain access to the rest of the server.",
delete_after=10
)
+
+ log.trace(f"Deleting the message posted by {ctx.author}")
await ctx.message.delete()
@command(name="accept", hidden=True, aliases=["verify", "verified", "accepted", "accept()"])
@@ -43,10 +52,13 @@ class Verification:
Accept our rules and gain access to the rest of the server
"""
+ log.debug(f"{ctx.author} called self.accept(). Assigning the user 'Developer' role.")
await ctx.author.add_roles(Object(VERIFIED_ROLE), reason="Accepted the rules")
+
+ log.trace(f"Deleting the message posted by {ctx.author}.")
await ctx.message.delete()
def setup(bot):
bot.add_cog(Verification(bot))
- print("Cog loaded: Verification")
+ log.info("Cog loaded: Verification")
diff --git a/bot/constants.py b/bot/constants.py
index b6e89a31a..3f002e6ef 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -12,6 +12,7 @@ PYTHON_CHANNEL = 267624335836053506
DEVLOG_CHANNEL = 409308876241108992
DEVTEST_CHANNEL = 414574275865870337
VERIFICATION_CHANNEL = 352442727016693763
+CHECKPOINT_TEST_CHANNEL = 422077681434099723
ADMIN_ROLE = 267628507062992896
MODERATOR_ROLE = 267629731250176001
diff --git a/bot/decorators.py b/bot/decorators.py
index d76812341..7009e259c 100644
--- a/bot/decorators.py
+++ b/bot/decorators.py
@@ -1,16 +1,26 @@
# coding=utf-8
+import logging
+
from discord.ext import commands
from discord.ext.commands import Context
+log = logging.getLogger(__name__)
+
def with_role(*role_ids: int):
async def predicate(ctx: Context):
if not ctx.guild: # Return False in a DM
+ log.debug(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
+ "This command is restricted by the with_role decorator. Rejecting request.")
return False
for role in ctx.author.roles:
if role.id in role_ids:
+ log.debug(f"{ctx.author} has the '{role.name}' role, and passes the check.")
return True
+
+ log.debug(f"{ctx.author} does not have the required role to use "
+ f"the '{ctx.command.name}' command, so the request is rejected.")
return False
return commands.check(predicate)
@@ -18,14 +28,22 @@ def with_role(*role_ids: int):
def without_role(*role_ids: int):
async def predicate(ctx: Context):
if not ctx.guild: # Return False in a DM
+ log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
+ "This command is restricted by the without_role decorator. Rejecting request.")
return False
author_roles = [role.id for role in ctx.author.roles]
- return all(role not in author_roles for role in role_ids)
+ check = all(role not in author_roles for role in role_ids)
+ log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The result of the without_role check was {check}.")
+ return check
return commands.check(predicate)
def in_channel(channel_id):
async def predicate(ctx: Context):
- return ctx.channel.id == channel_id
+ check = ctx.channel.id == channel_id
+ log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The result of the in_channel check was {check}.")
+ return check
return commands.check(predicate)
diff --git a/bot/formatter.py b/bot/formatter.py
index cb7c99b88..5b75d6a03 100644
--- a/bot/formatter.py
+++ b/bot/formatter.py
@@ -7,12 +7,15 @@ 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 HELP_PREFIX
+log = logging.getLogger(__name__)
+
class Formatter(HelpFormatter):
def __init__(self, *args, **kwargs):
@@ -24,6 +27,7 @@ class Formatter(HelpFormatter):
- 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
@@ -52,10 +56,11 @@ class Formatter(HelpFormatter):
# <ending help note>
"""
+
self._paginator = Paginator(prefix="```py")
if isinstance(self.command, Command):
- # strip the command of bot. and ()
+ # strip the command off bot. and ()
stripped_command = self.command.name.replace(HELP_PREFIX, "").replace("()", "")
# get the args using the handy inspect module
diff --git a/bot/pagination.py b/bot/pagination.py
index 51ddad212..268f34748 100644
--- a/bot/pagination.py
+++ b/bot/pagination.py
@@ -1,5 +1,6 @@
# coding=utf-8
import asyncio
+import logging
from typing import Iterable, Optional
from discord import Embed, Member, Reaction
@@ -14,6 +15,8 @@ LAST_EMOJI = "\u23ED"
PAGINATION_EMOJI = [FIRST_EMOJI, LEFT_EMOJI, RIGHT_EMOJI, LAST_EMOJI, DELETE_EMOJI]
+log = logging.getLogger(__name__)
+
class LinePaginator(Paginator):
"""
@@ -150,14 +153,24 @@ class LinePaginator(Paginator):
current_page = 0
for line in lines:
- paginator.add_line(line, empty=empty)
+ try:
+ paginator.add_line(line, empty=empty)
+ except Exception:
+ log.exception(f"Failed to add line to paginator: '{line}'")
+ raise # Should propagate
+ else:
+ log.trace(f"Added line to paginator: '{line}'")
+
+ log.debug(f"Paginator created with {len(paginator.pages)} pages")
embed.description = paginator.pages[current_page]
if len(paginator.pages) <= 1:
if footer_text:
embed.set_footer(text=footer_text)
+ log.trace(f"Setting embed footer to '{footer_text}'")
+ log.debug("There's less than two pages, so we won't paginate - sending single page on its own")
return await ctx.send(embed=embed)
else:
if footer_text:
@@ -165,24 +178,36 @@ class LinePaginator(Paginator):
else:
embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ log.trace(f"Setting embed footer to '{embed.footer.text}'")
+
+ log.debug("Sending first page to channel...")
message = await ctx.send(embed=embed)
+ log.debug("Adding emoji reactions to message...")
+
for emoji in PAGINATION_EMOJI:
# Add all the applicable emoji to the message
+ log.trace(f"Adding reaction: {repr(emoji)}")
await message.add_reaction(emoji)
while True:
try:
reaction, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=event_check)
+ log.trace(f"Got reaction: {reaction}")
except asyncio.TimeoutError:
+ log.debug("Timed out waiting for a reaction")
break # We're done, no reactions for the last 5 minutes
if reaction.emoji == DELETE_EMOJI:
+ log.debug("Got delete reaction")
break
if reaction.emoji == FIRST_EMOJI:
await message.remove_reaction(reaction.emoji, user)
current_page = 0
+
+ log.debug(f"Got first page reaction - changing to page 1/{len(paginator.pages)}")
+
embed.description = paginator.pages[current_page]
if footer_text:
embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
@@ -193,6 +218,9 @@ class LinePaginator(Paginator):
if reaction.emoji == LAST_EMOJI:
await message.remove_reaction(reaction.emoji, user)
current_page = len(paginator.pages) - 1
+
+ log.debug(f"Got last page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
+
embed.description = paginator.pages[current_page]
if footer_text:
embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
@@ -204,9 +232,11 @@ class LinePaginator(Paginator):
await message.remove_reaction(reaction.emoji, user)
if current_page <= 0:
+ log.debug("Got previous page reaction, but we're on the first page - ignoring")
continue
current_page -= 1
+ log.debug(f"Got previous page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
embed.description = paginator.pages[current_page]
@@ -221,9 +251,11 @@ class LinePaginator(Paginator):
await message.remove_reaction(reaction.emoji, user)
if current_page >= len(paginator.pages) - 1:
+ log.debug("Got next page reaction, but we're on the last page - ignoring")
continue
current_page += 1
+ log.debug(f"Got next page reaction - changing to page {current_page + 1}/{len(paginator.pages)}")
embed.description = paginator.pages[current_page]
@@ -234,4 +266,5 @@ class LinePaginator(Paginator):
await message.edit(embed=embed)
+ log.debug("Ending pagination and removing all reactions...")
await message.clear_reactions()
diff --git a/requirements.txt b/requirements.txt
index e0159eded..cf3742ffb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,4 @@ dulwich
multidict
sympy
aiodns
+logmatic-python
diff --git a/scripts/vagrant_bootstrap.sh b/scripts/vagrant_bootstrap.sh
index a9819fa4f..61e2835f4 100644
--- a/scripts/vagrant_bootstrap.sh
+++ b/scripts/vagrant_bootstrap.sh
@@ -14,3 +14,50 @@ apt-get install -y python3.6-dev
apt-get install -y build-essential
curl -s https://bootstrap.pypa.io/get-pip.py | python3.6 -
python3.6 -m pip install -r /vagrant/requirements.txt
+
+tee /root/.bashrc <<EOF
+HISTCONTROL=ignoreboth
+shopt -s histappend
+HISTSIZE=1000
+HISTFILESIZE=2000
+shopt -s checkwinsize
+
+if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
+ debian_chroot=$(cat /etc/debian_chroot)
+fi
+
+case "$TERM" in
+ xterm-color|*-256color) color_prompt=yes;;
+esac
+
+PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
+
+test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
+alias ls='ls --color=auto'
+alias grep='grep --color=auto'
+alias fgrep='fgrep --color=auto'
+alias egrep='egrep --color=auto'
+alias ll='ls -alF --color=auto'
+alias la='ls -A --color=auto'
+alias l='ls -CF --color=auto'
+
+export BOT_TOKEN="abcdefg"
+export SITE_URL="pysite.local"
+export DEPLOY_SITE_KEY="sdfsdf"
+export DEPLOY_BOT_KEY="sdfsdf"
+export DEPLOY_URL="https://api.beardfist.com/pythondiscord"
+export STATUS_URL="https://api.beardfist.com/pdstatus"
+export CLICKUP_KEY="abcdefg"
+export PAPERTRAIL_ADDRESS=""
+export PAPERTRAIL_PORT=""
+export LOG_LEVEL=DEBUG
+export SERVER_NAME="pysite.local"
+export WEBPAGE_PORT="80"
+export WEBPAGE_SECRET_KEY="123456789abcdefghijklmn"
+export RETHINKDB_HOST="127.0.0.1"
+export RETHINKDB_PORT="28016"
+export RETHINKDB_DATABASE="database"
+export RETHINKDB_TABLE="table"
+export BOT_API_KEY="abcdefghijklmnopqrstuvwxyz"
+export TEMPLATES_AUTO_RELOAD="yes"
+EOF