diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | bot/__init__.py | 83 | ||||
-rw-r--r-- | bot/cogs/bot.py | 87 | ||||
-rw-r--r-- | bot/cogs/clickup.py | 60 | ||||
-rw-r--r-- | bot/cogs/cogs.py | 178 | ||||
-rw-r--r-- | bot/cogs/deployment.py | 16 | ||||
-rw-r--r-- | bot/cogs/eval.py | 7 | ||||
-rw-r--r-- | bot/cogs/events.py | 25 | ||||
-rw-r--r-- | bot/cogs/fun.py | 13 | ||||
-rw-r--r-- | bot/cogs/logging.py | 8 | ||||
-rw-r--r-- | bot/cogs/security.py | 6 | ||||
-rw-r--r-- | bot/cogs/tags.py | 82 | ||||
-rw-r--r-- | bot/cogs/verification.py | 18 | ||||
-rw-r--r-- | bot/constants.py | 1 | ||||
-rw-r--r-- | bot/decorators.py | 22 | ||||
-rw-r--r-- | bot/formatter.py | 7 | ||||
-rw-r--r-- | bot/pagination.py | 35 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | scripts/vagrant_bootstrap.sh | 47 |
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 |