From 70fa9a8e36d586d6fbc1690a80a598e94506883b Mon Sep 17 00:00:00 2001 From: janine9vn Date: Fri, 9 Apr 2021 12:52:04 -0400 Subject: Cutover of rattlesnake to lancebot This is an initial cutover of the rattlesnake internal eval to Sir Lancebot. This commit by itself will not work. This is a simple drop in of rattlesnake code so there is context as to what has changed and why. --- bot/exts/internal_eval/__init__.py | 0 bot/exts/internal_eval/internal_eval.py | 152 ++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 bot/exts/internal_eval/__init__.py create mode 100644 bot/exts/internal_eval/internal_eval.py (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/__init__.py b/bot/exts/internal_eval/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bot/exts/internal_eval/internal_eval.py b/bot/exts/internal_eval/internal_eval.py new file mode 100644 index 00000000..f6812942 --- /dev/null +++ b/bot/exts/internal_eval/internal_eval.py @@ -0,0 +1,152 @@ +import logging +import re +import textwrap +import typing + +import discord +from discord.ext import commands + +from rattlesnake.bot import Rattlesnake +from rattlesnake.constants import ADMIN_ROLES +from rattlesnake.utils import in_whitelist +from .helpers import EvalContext + +__all__ = ["InternalEval"] + +log = logging.getLogger("rattlesnake.exts.admin_tools.internal_eval") + +CODEBLOCK_REGEX = re.compile(r"(^```(py(thon)?)?\n)|(```$)") + + +class InternalEval(commands.Cog): + """Top secret code evaluation for admins and owners.""" + + def __init__(self, bot: Rattlesnake): + self.bot = bot + self.locals = {} + + @staticmethod + def shorten_output( + output: str, + max_length: int = 1900, + placeholder: str = "\n[output truncated]" + ) -> str: + """ + Shorten the `output` so it's shorter than `max_length`. + There are three tactics for this, tried in the following order: + - Shorten the output on a line-by-line basis + - Shorten the output on any whitespace character + - Shorten the output solely on character count + """ + max_length = max_length - len(placeholder) + + shortened_output = [] + char_count = 0 + for line in output.split("\n"): + if char_count + len(line) > max_length: + break + shortened_output.append(line) + char_count += len(line) + 1 # account for (possible) line ending + + if shortened_output: + shortened_output.append(placeholder) + return "\n".join(shortened_output) + + shortened_output = textwrap.shorten(output, width=max_length, placeholder=placeholder) + + if shortened_output.strip() == placeholder.strip(): + # `textwrap` was unable to find whitespace to shorten on, so it has + # reduced the output to just the placeholder. Let's shorten based on + # characters instead. + shortened_output = output[:max_length] + placeholder + + return shortened_output + + async def _upload_output(self, output: str) -> typing.Optional[str]: + """Upload `internal eval` output to our pastebin and return the url.""" + try: + async with self.bot.http_session.post( + "https://paste.pythondiscord.com/documents", data=output, raise_for_status=True + ) as resp: + data = await resp.json() + + if "key" in data: + return f"https://paste.pythondiscord.com/{data['key']}" + except Exception: + # 400 (Bad Request) means there are too many characters + log.exception("Failed to upload `internal eval` output to paste service!") + + async def _send_output(self, ctx: commands.Context, output: str) -> None: + """Send the `internal eval` output to the command invocation context.""" + upload_message = "" + if len(output) >= 1980: + # The output is too long, let's truncate it for in-channel output and + # upload the complete output to the paste service. + url = await self._upload_output(output) + + if url: + upload_message = f"\nFull output here: {url}" + else: + upload_message = "\n:warning: Failed to upload full output!" + + output = self.shorten_output(output) + + await ctx.send(f"```py\n{output}```{upload_message}") + + async def _eval(self, ctx: commands.Context, code: str) -> None: + """Evaluate the `code` in the current evaluation context.""" + if code.startswith("exit"): + self.locals = {} + await ctx.send("The evaluation context was reset.") + return + + context_vars = { + "message": ctx.message, + "author": ctx.message.author, + "channel": ctx.channel, + "guild": ctx.guild, + "ctx": ctx, + "self": self, + "bot": self.bot, + "discord": discord, + } + + eval_context = EvalContext(context_vars, self.locals) + + log.trace("Preparing the evaluation by parsing the AST of the code") + error = eval_context.prepare_eval(code) + + if error: + log.trace("The code can't be evaluated due to an error") + await ctx.send(f"```py\n{error}\n```") + return + + log.trace("Evaluate the AST we've generated for the evaluation") + new_locals = await eval_context.run_eval() + + log.trace("Updating locals with those set during evaluation") + self.locals.update(new_locals) + + log.trace("Sending the formatted output back to the context") + await self._send_output(ctx, eval_context.format_output()) + + @commands.group(name='internal', aliases=('int',)) + @in_whitelist(roles=ADMIN_ROLES) + async def internal_group(self, ctx: commands.Context) -> None: + """Internal commands. Top secret!""" + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) + + @internal_group.command(name='eval', aliases=('e',)) + @in_whitelist(roles=ADMIN_ROLES) + async def eval(self, ctx: commands.Context, *, code: str) -> None: + """Run eval in a REPL-like format.""" + code = CODEBLOCK_REGEX.sub("", code.strip()) + await self._eval(ctx, code) + + @internal_group.command(name='reset', aliases=("clear", "exit", "r", "c")) + @in_whitelist(roles=ADMIN_ROLES) + async def reset(self, ctx: commands.Context) -> None: + """Run eval in a REPL-like format.""" + self.locals = {} + await ctx.send("The evaluation context was reset.") -- cgit v1.2.3 From 8ca0fd85045bf9fbb9b78a117f3a2e30ee1d1fa5 Mon Sep 17 00:00:00 2001 From: janine9vn Date: Fri, 9 Apr 2021 12:54:59 -0400 Subject: Add helpers for internal eval Cutting over the rattlesnake helpers specifically for internal_eval. I am mirroring the rattlesnake structure as much as I can initially to ensure basic functionality before migrating functions to fit more within Sir Lancebot's file structure. --- bot/exts/internal_eval/helpers.py | 243 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 bot/exts/internal_eval/helpers.py (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/helpers.py b/bot/exts/internal_eval/helpers.py new file mode 100644 index 00000000..5c602e4d --- /dev/null +++ b/bot/exts/internal_eval/helpers.py @@ -0,0 +1,243 @@ +import ast +import collections +import contextlib +import functools +import inspect +import io +import logging +import sys +import traceback +import types +import typing + + +log = logging.getLogger("rattlesnake.exts.admin_tools.internal_eval") + +# A type alias to annotate the tuples returned from `sys.exc_info()` +ExcInfo = typing.Tuple[typing.Type[Exception], Exception, types.TracebackType] +Namespace = typing.Dict[str, typing.Any] + +# This will be used as an coroutine function wrapper for the code +# to be evaluated. The wrapper contains one `pass` statement which +# will be replaced with `ast` with the code that we want to have +# evaluated. +# The function redirects output and captures exceptions that were +# raised in the code we evaluate. The latter is used to provide a +# meaningful traceback to the end user. +EVAL_WRAPPER = """ +async def _eval_wrapper_function(): + try: + with contextlib.redirect_stdout(_eval_context.stdout): + pass + if '_value_last_expression' in locals(): + if inspect.isawaitable(_value_last_expression): + _value_last_expression = await _value_last_expression + _eval_context._value_last_expression = _value_last_expression + else: + _eval_context._value_last_expression = None + except Exception: + _eval_context.exc_info = sys.exc_info() + finally: + _eval_context.locals = locals() +_eval_context.function = _eval_wrapper_function +""" + + +def format_internal_eval_exception(exc_info: ExcInfo, code: str) -> str: + """Format an exception caught while evaluation code by inserting lines.""" + exc_type, exc_value, tb = exc_info + stack_summary = traceback.StackSummary.extract(traceback.walk_tb(tb)) + code = code.split("\n") + + output = ["Traceback (most recent call last):"] + for frame in stack_summary: + if frame.filename == "": + line = code[frame.lineno - 1].lstrip() + + if frame.name == "_eval_wrapper_function": + name = "" + else: + name = frame.name + else: + line = frame.line + name = frame.name + + output.append( + f' File "{frame.filename}", line {frame.lineno}, in {name}\n' + f" {line}" + ) + + output.extend(traceback.format_exception_only(exc_type, exc_value)) + return "\n".join(output) + + +class EvalContext: + """ + Represents the current `internal eval` context. + The context remembers names set during earlier runs of `internal eval`. To + clear the context, use the `?internal clear` command. + """ + + def __init__(self, context_vars: Namespace, local_vars: Namespace) -> None: + self._locals = dict(local_vars) + self.context_vars = dict(context_vars) + + self.stdout = io.StringIO() + self._value_last_expression = None + self.exc_info = None + self.code = "" + self.function = None + self.eval_tree = None + + @property + def dependencies(self) -> typing.Dict[str, typing.Any]: + """ + Return a mapping of the dependencies for the wrapper function. + By using a property descriptor, the mapping can't be accidentally + mutated during evaluation. This ensures the dependencies are always + available. + """ + return { + "print": functools.partial(print, file=self.stdout), + "contextlib": contextlib, + "inspect": inspect, + "sys": sys, + "_eval_context": self, + "_": self._value_last_expression, + } + + @property + def locals(self) -> typing.Dict[str, typing.Any]: + """Return a mapping of names->values needed for evaluation.""" + return {**collections.ChainMap(self.dependencies, self.context_vars, self._locals)} + + @locals.setter + def locals(self, locals_: typing.Dict[str, typing.Any]) -> None: + """Update the contextual mapping of names to values.""" + log.trace(f"Updating {self._locals} with {locals_}") + self._locals.update(locals_) + + def prepare_eval(self, code: str) -> typing.Optional[str]: + """Prepare an evaluation by processing the code and setting up the context.""" + self.code = code + + if not self.code: + log.debug("No code was attached to the evaluation command") + return "[No code detected]" + + try: + code_tree = ast.parse(code, filename="") + except SyntaxError: + log.debug("Got a SyntaxError while parsing the eval code") + return "".join(traceback.format_exception(*sys.exc_info(), limit=0)) + + log.trace("Parsing the AST to see if there's a trailing expression we need to capture") + code_tree = CaptureLastExpression(code_tree).capture() + + log.trace("Wrapping the AST in the AST of the wrapper coroutine") + eval_tree = WrapEvalCodeTree(code_tree).wrap() + + self.eval_tree = eval_tree + return None + + async def run_eval(self) -> Namespace: + """Run the evaluation and return the updated locals.""" + log.trace("Compiling the AST to bytecode using `exec` mode") + compiled_code = compile(self.eval_tree, filename="", mode="exec") + + log.trace("Executing the compiled code with the desired namespace environment") + exec(compiled_code, self.locals) # noqa: B102,S102 + + log.trace("Awaiting the created evaluation wrapper coroutine.") + await self.function() + + log.trace("Returning the updated captured locals.") + return self._locals + + def format_output(self) -> str: + """Format the output of the most recent evaluation.""" + output = [] + + log.trace(f"Getting output from stdout `{id(self.stdout)}`") + stdout_text = self.stdout.getvalue() + if stdout_text: + log.trace("Appending output captured from stdout/print") + output.append(stdout_text) + + if self._value_last_expression is not None: + log.trace("Appending the output of a captured trialing expression") + output.append(f"[Captured] {self._value_last_expression!r}") + + if self.exc_info: + log.trace("Appending exception information") + output.append(format_internal_eval_exception(self.exc_info, self.code)) + + log.trace(f"Generated output: {output!r}") + return "\n".join(output) or "[No output]" + + +class WrapEvalCodeTree(ast.NodeTransformer): + """Wraps the AST of eval code with the wrapper function.""" + + def __init__(self, eval_code_tree: ast.AST, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.eval_code_tree = eval_code_tree + + # To avoid mutable aliasing, parse the WRAPPER_FUNC for each wrapping + self.wrapper = ast.parse(EVAL_WRAPPER, filename="") + + def wrap(self) -> ast.AST: + """Wrap the tree of the code by the tree of the wrapper function.""" + new_tree = self.visit(self.wrapper) + return ast.fix_missing_locations(new_tree) + + def visit_Pass(self, node: ast.Pass) -> typing.List[ast.AST]: # noqa: N802 + """ + Replace the `_ast.Pass` node in the wrapper function by the eval AST. + This method works on the assumption that there's a single `pass` + statement in the wrapper function. + """ + return list(ast.iter_child_nodes(self.eval_code_tree)) + + +class CaptureLastExpression(ast.NodeTransformer): + """Captures the return value from a loose expression.""" + + def __init__(self, tree: ast.AST, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.tree = tree + self.last_node = list(ast.iter_child_nodes(tree))[-1] + + def visit_Expr(self, node: ast.Expr) -> typing.Union[ast.Expr, ast.Assign]: # noqa: N802 + """ + Replace the Expr node that is last child node of Module with an assignment. + We use an assignment to capture the value of the last node, if it's a loose + Expr node. Normally, the value of an Expr node is lost, meaning we don't get + the output of such a last "loose" expression. By assigning it a name, we can + retrieve it for our output. + """ + if node is not self.last_node: + return node + + log.trace("Found a trailing last expression in the evaluation code") + + log.trace("Creating assignment statement with trailing expression as the right-hand side") + right_hand_side = list(ast.iter_child_nodes(node))[0] + + assignment = ast.Assign( + targets=[ast.Name(id='_value_last_expression', ctx=ast.Store())], + value=right_hand_side, + lineno=node.lineno, + col_offset=0, + ) + ast.fix_missing_locations(assignment) + return assignment + + def capture(self) -> ast.AST: + """Capture the value of the last expression with an assignment.""" + if not isinstance(self.last_node, ast.Expr): + # We only have to replace a node if the very last node is an Expr node + return self.tree + + new_tree = self.visit(self.tree) + return ast.fix_missing_locations(new_tree) -- cgit v1.2.3 From 80a9a40b3c27376657486cf183d4c0c7d4e9880f Mon Sep 17 00:00:00 2001 From: janine9vn Date: Fri, 9 Apr 2021 13:41:45 -0400 Subject: Realigned to SirLancebot Structure The code for rattlesnakes's internal eval was aligned to Sir Lancebot's structure. It was mostly renaming rattlesnake to bot and changing how some of the imports were setup as. It also included changing the __init__.py to match the Sir Lancebot cog structure. Additionally, the whitelist check has been significantly simplified to only be a role check for the admin role. The rattlesnake implementation had a more robust `in_whitelist` decorator, so it may be worth investigating adding that in if we see fit. For now, it's a simple `with_role` decorator check. The name of the cog file itself was changed to include an underscore to sidestep what I think was a namespace collision that would prevent the setup function from properly running. --- bot/exts/internal_eval/__init__.py | 10 ++ bot/exts/internal_eval/_helpers.py | 243 +++++++++++++++++++++++++++++++ bot/exts/internal_eval/_internal_eval.py | 152 +++++++++++++++++++ 3 files changed, 405 insertions(+) create mode 100644 bot/exts/internal_eval/_helpers.py create mode 100644 bot/exts/internal_eval/_internal_eval.py (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/__init__.py b/bot/exts/internal_eval/__init__.py index e69de29b..695fa74d 100644 --- a/bot/exts/internal_eval/__init__.py +++ b/bot/exts/internal_eval/__init__.py @@ -0,0 +1,10 @@ +from bot.bot import Bot + + +def setup(bot: Bot) -> None: + """Set up the Internal Eval extension.""" + # Import the Cog at runtime to prevent side effects like defining + # RedisCache instances too early. + from ._internal_eval import InternalEval + + bot.add_cog(InternalEval(bot)) diff --git a/bot/exts/internal_eval/_helpers.py b/bot/exts/internal_eval/_helpers.py new file mode 100644 index 00000000..5c602e4d --- /dev/null +++ b/bot/exts/internal_eval/_helpers.py @@ -0,0 +1,243 @@ +import ast +import collections +import contextlib +import functools +import inspect +import io +import logging +import sys +import traceback +import types +import typing + + +log = logging.getLogger("rattlesnake.exts.admin_tools.internal_eval") + +# A type alias to annotate the tuples returned from `sys.exc_info()` +ExcInfo = typing.Tuple[typing.Type[Exception], Exception, types.TracebackType] +Namespace = typing.Dict[str, typing.Any] + +# This will be used as an coroutine function wrapper for the code +# to be evaluated. The wrapper contains one `pass` statement which +# will be replaced with `ast` with the code that we want to have +# evaluated. +# The function redirects output and captures exceptions that were +# raised in the code we evaluate. The latter is used to provide a +# meaningful traceback to the end user. +EVAL_WRAPPER = """ +async def _eval_wrapper_function(): + try: + with contextlib.redirect_stdout(_eval_context.stdout): + pass + if '_value_last_expression' in locals(): + if inspect.isawaitable(_value_last_expression): + _value_last_expression = await _value_last_expression + _eval_context._value_last_expression = _value_last_expression + else: + _eval_context._value_last_expression = None + except Exception: + _eval_context.exc_info = sys.exc_info() + finally: + _eval_context.locals = locals() +_eval_context.function = _eval_wrapper_function +""" + + +def format_internal_eval_exception(exc_info: ExcInfo, code: str) -> str: + """Format an exception caught while evaluation code by inserting lines.""" + exc_type, exc_value, tb = exc_info + stack_summary = traceback.StackSummary.extract(traceback.walk_tb(tb)) + code = code.split("\n") + + output = ["Traceback (most recent call last):"] + for frame in stack_summary: + if frame.filename == "": + line = code[frame.lineno - 1].lstrip() + + if frame.name == "_eval_wrapper_function": + name = "" + else: + name = frame.name + else: + line = frame.line + name = frame.name + + output.append( + f' File "{frame.filename}", line {frame.lineno}, in {name}\n' + f" {line}" + ) + + output.extend(traceback.format_exception_only(exc_type, exc_value)) + return "\n".join(output) + + +class EvalContext: + """ + Represents the current `internal eval` context. + The context remembers names set during earlier runs of `internal eval`. To + clear the context, use the `?internal clear` command. + """ + + def __init__(self, context_vars: Namespace, local_vars: Namespace) -> None: + self._locals = dict(local_vars) + self.context_vars = dict(context_vars) + + self.stdout = io.StringIO() + self._value_last_expression = None + self.exc_info = None + self.code = "" + self.function = None + self.eval_tree = None + + @property + def dependencies(self) -> typing.Dict[str, typing.Any]: + """ + Return a mapping of the dependencies for the wrapper function. + By using a property descriptor, the mapping can't be accidentally + mutated during evaluation. This ensures the dependencies are always + available. + """ + return { + "print": functools.partial(print, file=self.stdout), + "contextlib": contextlib, + "inspect": inspect, + "sys": sys, + "_eval_context": self, + "_": self._value_last_expression, + } + + @property + def locals(self) -> typing.Dict[str, typing.Any]: + """Return a mapping of names->values needed for evaluation.""" + return {**collections.ChainMap(self.dependencies, self.context_vars, self._locals)} + + @locals.setter + def locals(self, locals_: typing.Dict[str, typing.Any]) -> None: + """Update the contextual mapping of names to values.""" + log.trace(f"Updating {self._locals} with {locals_}") + self._locals.update(locals_) + + def prepare_eval(self, code: str) -> typing.Optional[str]: + """Prepare an evaluation by processing the code and setting up the context.""" + self.code = code + + if not self.code: + log.debug("No code was attached to the evaluation command") + return "[No code detected]" + + try: + code_tree = ast.parse(code, filename="") + except SyntaxError: + log.debug("Got a SyntaxError while parsing the eval code") + return "".join(traceback.format_exception(*sys.exc_info(), limit=0)) + + log.trace("Parsing the AST to see if there's a trailing expression we need to capture") + code_tree = CaptureLastExpression(code_tree).capture() + + log.trace("Wrapping the AST in the AST of the wrapper coroutine") + eval_tree = WrapEvalCodeTree(code_tree).wrap() + + self.eval_tree = eval_tree + return None + + async def run_eval(self) -> Namespace: + """Run the evaluation and return the updated locals.""" + log.trace("Compiling the AST to bytecode using `exec` mode") + compiled_code = compile(self.eval_tree, filename="", mode="exec") + + log.trace("Executing the compiled code with the desired namespace environment") + exec(compiled_code, self.locals) # noqa: B102,S102 + + log.trace("Awaiting the created evaluation wrapper coroutine.") + await self.function() + + log.trace("Returning the updated captured locals.") + return self._locals + + def format_output(self) -> str: + """Format the output of the most recent evaluation.""" + output = [] + + log.trace(f"Getting output from stdout `{id(self.stdout)}`") + stdout_text = self.stdout.getvalue() + if stdout_text: + log.trace("Appending output captured from stdout/print") + output.append(stdout_text) + + if self._value_last_expression is not None: + log.trace("Appending the output of a captured trialing expression") + output.append(f"[Captured] {self._value_last_expression!r}") + + if self.exc_info: + log.trace("Appending exception information") + output.append(format_internal_eval_exception(self.exc_info, self.code)) + + log.trace(f"Generated output: {output!r}") + return "\n".join(output) or "[No output]" + + +class WrapEvalCodeTree(ast.NodeTransformer): + """Wraps the AST of eval code with the wrapper function.""" + + def __init__(self, eval_code_tree: ast.AST, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.eval_code_tree = eval_code_tree + + # To avoid mutable aliasing, parse the WRAPPER_FUNC for each wrapping + self.wrapper = ast.parse(EVAL_WRAPPER, filename="") + + def wrap(self) -> ast.AST: + """Wrap the tree of the code by the tree of the wrapper function.""" + new_tree = self.visit(self.wrapper) + return ast.fix_missing_locations(new_tree) + + def visit_Pass(self, node: ast.Pass) -> typing.List[ast.AST]: # noqa: N802 + """ + Replace the `_ast.Pass` node in the wrapper function by the eval AST. + This method works on the assumption that there's a single `pass` + statement in the wrapper function. + """ + return list(ast.iter_child_nodes(self.eval_code_tree)) + + +class CaptureLastExpression(ast.NodeTransformer): + """Captures the return value from a loose expression.""" + + def __init__(self, tree: ast.AST, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.tree = tree + self.last_node = list(ast.iter_child_nodes(tree))[-1] + + def visit_Expr(self, node: ast.Expr) -> typing.Union[ast.Expr, ast.Assign]: # noqa: N802 + """ + Replace the Expr node that is last child node of Module with an assignment. + We use an assignment to capture the value of the last node, if it's a loose + Expr node. Normally, the value of an Expr node is lost, meaning we don't get + the output of such a last "loose" expression. By assigning it a name, we can + retrieve it for our output. + """ + if node is not self.last_node: + return node + + log.trace("Found a trailing last expression in the evaluation code") + + log.trace("Creating assignment statement with trailing expression as the right-hand side") + right_hand_side = list(ast.iter_child_nodes(node))[0] + + assignment = ast.Assign( + targets=[ast.Name(id='_value_last_expression', ctx=ast.Store())], + value=right_hand_side, + lineno=node.lineno, + col_offset=0, + ) + ast.fix_missing_locations(assignment) + return assignment + + def capture(self) -> ast.AST: + """Capture the value of the last expression with an assignment.""" + if not isinstance(self.last_node, ast.Expr): + # We only have to replace a node if the very last node is an Expr node + return self.tree + + new_tree = self.visit(self.tree) + return ast.fix_missing_locations(new_tree) diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py new file mode 100644 index 00000000..f7a0946b --- /dev/null +++ b/bot/exts/internal_eval/_internal_eval.py @@ -0,0 +1,152 @@ +import logging +import re +import textwrap +import typing + +import discord +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Roles +from bot.utils.decorators import with_role +from ._helpers import EvalContext + +__all__ = ["InternalEval"] + +log = logging.getLogger("rattlesnake.exts.admin_tools.internal_eval") + +CODEBLOCK_REGEX = re.compile(r"(^```(py(thon)?)?\n)|(```$)") + + +class InternalEval(commands.Cog): + """Top secret code evaluation for admins and owners.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.locals = {} + + @staticmethod + def shorten_output( + output: str, + max_length: int = 1900, + placeholder: str = "\n[output truncated]" + ) -> str: + """ + Shorten the `output` so it's shorter than `max_length`. + There are three tactics for this, tried in the following order: + - Shorten the output on a line-by-line basis + - Shorten the output on any whitespace character + - Shorten the output solely on character count + """ + max_length = max_length - len(placeholder) + + shortened_output = [] + char_count = 0 + for line in output.split("\n"): + if char_count + len(line) > max_length: + break + shortened_output.append(line) + char_count += len(line) + 1 # account for (possible) line ending + + if shortened_output: + shortened_output.append(placeholder) + return "\n".join(shortened_output) + + shortened_output = textwrap.shorten(output, width=max_length, placeholder=placeholder) + + if shortened_output.strip() == placeholder.strip(): + # `textwrap` was unable to find whitespace to shorten on, so it has + # reduced the output to just the placeholder. Let's shorten based on + # characters instead. + shortened_output = output[:max_length] + placeholder + + return shortened_output + + async def _upload_output(self, output: str) -> typing.Optional[str]: + """Upload `internal eval` output to our pastebin and return the url.""" + try: + async with self.bot.http_session.post( + "https://paste.pythondiscord.com/documents", data=output, raise_for_status=True + ) as resp: + data = await resp.json() + + if "key" in data: + return f"https://paste.pythondiscord.com/{data['key']}" + except Exception: + # 400 (Bad Request) means there are too many characters + log.exception("Failed to upload `internal eval` output to paste service!") + + async def _send_output(self, ctx: commands.Context, output: str) -> None: + """Send the `internal eval` output to the command invocation context.""" + upload_message = "" + if len(output) >= 1980: + # The output is too long, let's truncate it for in-channel output and + # upload the complete output to the paste service. + url = await self._upload_output(output) + + if url: + upload_message = f"\nFull output here: {url}" + else: + upload_message = "\n:warning: Failed to upload full output!" + + output = self.shorten_output(output) + + await ctx.send(f"```py\n{output}```{upload_message}") + + async def _eval(self, ctx: commands.Context, code: str) -> None: + """Evaluate the `code` in the current evaluation context.""" + if code.startswith("exit"): + self.locals = {} + await ctx.send("The evaluation context was reset.") + return + + context_vars = { + "message": ctx.message, + "author": ctx.message.author, + "channel": ctx.channel, + "guild": ctx.guild, + "ctx": ctx, + "self": self, + "bot": self.bot, + "discord": discord, + } + + eval_context = EvalContext(context_vars, self.locals) + + log.trace("Preparing the evaluation by parsing the AST of the code") + error = eval_context.prepare_eval(code) + + if error: + log.trace("The code can't be evaluated due to an error") + await ctx.send(f"```py\n{error}\n```") + return + + log.trace("Evaluate the AST we've generated for the evaluation") + new_locals = await eval_context.run_eval() + + log.trace("Updating locals with those set during evaluation") + self.locals.update(new_locals) + + log.trace("Sending the formatted output back to the context") + await self._send_output(ctx, eval_context.format_output()) + + @commands.group(name='internal', aliases=('int',)) + @with_role(Roles.admin) + async def internal_group(self, ctx: commands.Context) -> None: + """Internal commands. Top secret!""" + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) + + @internal_group.command(name='eval', aliases=('e',)) + @with_role(Roles.admin) + async def eval(self, ctx: commands.Context, *, code: str) -> None: + """Run eval in a REPL-like format.""" + code = CODEBLOCK_REGEX.sub("", code.strip()) + await self._eval(ctx, code) + + @internal_group.command(name='reset', aliases=("clear", "exit", "r", "c")) + @with_role(Roles.admin) + async def reset(self, ctx: commands.Context) -> None: + """Run eval in a REPL-like format.""" + self.locals = {} + await ctx.send("The evaluation context was reset.") -- cgit v1.2.3 From 0ac739dc029664332f1416ba85424c3b396e5660 Mon Sep 17 00:00:00 2001 From: janine9vn Date: Sat, 10 Apr 2021 20:17:47 -0400 Subject: Change names These were missed in a previous commit. It's a simple name change from the original files to better align with Sir Lancebot. --- bot/exts/internal_eval/helpers.py | 243 -------------------------------- bot/exts/internal_eval/internal_eval.py | 152 -------------------- 2 files changed, 395 deletions(-) delete mode 100644 bot/exts/internal_eval/helpers.py delete mode 100644 bot/exts/internal_eval/internal_eval.py (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/helpers.py b/bot/exts/internal_eval/helpers.py deleted file mode 100644 index 5c602e4d..00000000 --- a/bot/exts/internal_eval/helpers.py +++ /dev/null @@ -1,243 +0,0 @@ -import ast -import collections -import contextlib -import functools -import inspect -import io -import logging -import sys -import traceback -import types -import typing - - -log = logging.getLogger("rattlesnake.exts.admin_tools.internal_eval") - -# A type alias to annotate the tuples returned from `sys.exc_info()` -ExcInfo = typing.Tuple[typing.Type[Exception], Exception, types.TracebackType] -Namespace = typing.Dict[str, typing.Any] - -# This will be used as an coroutine function wrapper for the code -# to be evaluated. The wrapper contains one `pass` statement which -# will be replaced with `ast` with the code that we want to have -# evaluated. -# The function redirects output and captures exceptions that were -# raised in the code we evaluate. The latter is used to provide a -# meaningful traceback to the end user. -EVAL_WRAPPER = """ -async def _eval_wrapper_function(): - try: - with contextlib.redirect_stdout(_eval_context.stdout): - pass - if '_value_last_expression' in locals(): - if inspect.isawaitable(_value_last_expression): - _value_last_expression = await _value_last_expression - _eval_context._value_last_expression = _value_last_expression - else: - _eval_context._value_last_expression = None - except Exception: - _eval_context.exc_info = sys.exc_info() - finally: - _eval_context.locals = locals() -_eval_context.function = _eval_wrapper_function -""" - - -def format_internal_eval_exception(exc_info: ExcInfo, code: str) -> str: - """Format an exception caught while evaluation code by inserting lines.""" - exc_type, exc_value, tb = exc_info - stack_summary = traceback.StackSummary.extract(traceback.walk_tb(tb)) - code = code.split("\n") - - output = ["Traceback (most recent call last):"] - for frame in stack_summary: - if frame.filename == "": - line = code[frame.lineno - 1].lstrip() - - if frame.name == "_eval_wrapper_function": - name = "" - else: - name = frame.name - else: - line = frame.line - name = frame.name - - output.append( - f' File "{frame.filename}", line {frame.lineno}, in {name}\n' - f" {line}" - ) - - output.extend(traceback.format_exception_only(exc_type, exc_value)) - return "\n".join(output) - - -class EvalContext: - """ - Represents the current `internal eval` context. - The context remembers names set during earlier runs of `internal eval`. To - clear the context, use the `?internal clear` command. - """ - - def __init__(self, context_vars: Namespace, local_vars: Namespace) -> None: - self._locals = dict(local_vars) - self.context_vars = dict(context_vars) - - self.stdout = io.StringIO() - self._value_last_expression = None - self.exc_info = None - self.code = "" - self.function = None - self.eval_tree = None - - @property - def dependencies(self) -> typing.Dict[str, typing.Any]: - """ - Return a mapping of the dependencies for the wrapper function. - By using a property descriptor, the mapping can't be accidentally - mutated during evaluation. This ensures the dependencies are always - available. - """ - return { - "print": functools.partial(print, file=self.stdout), - "contextlib": contextlib, - "inspect": inspect, - "sys": sys, - "_eval_context": self, - "_": self._value_last_expression, - } - - @property - def locals(self) -> typing.Dict[str, typing.Any]: - """Return a mapping of names->values needed for evaluation.""" - return {**collections.ChainMap(self.dependencies, self.context_vars, self._locals)} - - @locals.setter - def locals(self, locals_: typing.Dict[str, typing.Any]) -> None: - """Update the contextual mapping of names to values.""" - log.trace(f"Updating {self._locals} with {locals_}") - self._locals.update(locals_) - - def prepare_eval(self, code: str) -> typing.Optional[str]: - """Prepare an evaluation by processing the code and setting up the context.""" - self.code = code - - if not self.code: - log.debug("No code was attached to the evaluation command") - return "[No code detected]" - - try: - code_tree = ast.parse(code, filename="") - except SyntaxError: - log.debug("Got a SyntaxError while parsing the eval code") - return "".join(traceback.format_exception(*sys.exc_info(), limit=0)) - - log.trace("Parsing the AST to see if there's a trailing expression we need to capture") - code_tree = CaptureLastExpression(code_tree).capture() - - log.trace("Wrapping the AST in the AST of the wrapper coroutine") - eval_tree = WrapEvalCodeTree(code_tree).wrap() - - self.eval_tree = eval_tree - return None - - async def run_eval(self) -> Namespace: - """Run the evaluation and return the updated locals.""" - log.trace("Compiling the AST to bytecode using `exec` mode") - compiled_code = compile(self.eval_tree, filename="", mode="exec") - - log.trace("Executing the compiled code with the desired namespace environment") - exec(compiled_code, self.locals) # noqa: B102,S102 - - log.trace("Awaiting the created evaluation wrapper coroutine.") - await self.function() - - log.trace("Returning the updated captured locals.") - return self._locals - - def format_output(self) -> str: - """Format the output of the most recent evaluation.""" - output = [] - - log.trace(f"Getting output from stdout `{id(self.stdout)}`") - stdout_text = self.stdout.getvalue() - if stdout_text: - log.trace("Appending output captured from stdout/print") - output.append(stdout_text) - - if self._value_last_expression is not None: - log.trace("Appending the output of a captured trialing expression") - output.append(f"[Captured] {self._value_last_expression!r}") - - if self.exc_info: - log.trace("Appending exception information") - output.append(format_internal_eval_exception(self.exc_info, self.code)) - - log.trace(f"Generated output: {output!r}") - return "\n".join(output) or "[No output]" - - -class WrapEvalCodeTree(ast.NodeTransformer): - """Wraps the AST of eval code with the wrapper function.""" - - def __init__(self, eval_code_tree: ast.AST, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - self.eval_code_tree = eval_code_tree - - # To avoid mutable aliasing, parse the WRAPPER_FUNC for each wrapping - self.wrapper = ast.parse(EVAL_WRAPPER, filename="") - - def wrap(self) -> ast.AST: - """Wrap the tree of the code by the tree of the wrapper function.""" - new_tree = self.visit(self.wrapper) - return ast.fix_missing_locations(new_tree) - - def visit_Pass(self, node: ast.Pass) -> typing.List[ast.AST]: # noqa: N802 - """ - Replace the `_ast.Pass` node in the wrapper function by the eval AST. - This method works on the assumption that there's a single `pass` - statement in the wrapper function. - """ - return list(ast.iter_child_nodes(self.eval_code_tree)) - - -class CaptureLastExpression(ast.NodeTransformer): - """Captures the return value from a loose expression.""" - - def __init__(self, tree: ast.AST, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - self.tree = tree - self.last_node = list(ast.iter_child_nodes(tree))[-1] - - def visit_Expr(self, node: ast.Expr) -> typing.Union[ast.Expr, ast.Assign]: # noqa: N802 - """ - Replace the Expr node that is last child node of Module with an assignment. - We use an assignment to capture the value of the last node, if it's a loose - Expr node. Normally, the value of an Expr node is lost, meaning we don't get - the output of such a last "loose" expression. By assigning it a name, we can - retrieve it for our output. - """ - if node is not self.last_node: - return node - - log.trace("Found a trailing last expression in the evaluation code") - - log.trace("Creating assignment statement with trailing expression as the right-hand side") - right_hand_side = list(ast.iter_child_nodes(node))[0] - - assignment = ast.Assign( - targets=[ast.Name(id='_value_last_expression', ctx=ast.Store())], - value=right_hand_side, - lineno=node.lineno, - col_offset=0, - ) - ast.fix_missing_locations(assignment) - return assignment - - def capture(self) -> ast.AST: - """Capture the value of the last expression with an assignment.""" - if not isinstance(self.last_node, ast.Expr): - # We only have to replace a node if the very last node is an Expr node - return self.tree - - new_tree = self.visit(self.tree) - return ast.fix_missing_locations(new_tree) diff --git a/bot/exts/internal_eval/internal_eval.py b/bot/exts/internal_eval/internal_eval.py deleted file mode 100644 index f6812942..00000000 --- a/bot/exts/internal_eval/internal_eval.py +++ /dev/null @@ -1,152 +0,0 @@ -import logging -import re -import textwrap -import typing - -import discord -from discord.ext import commands - -from rattlesnake.bot import Rattlesnake -from rattlesnake.constants import ADMIN_ROLES -from rattlesnake.utils import in_whitelist -from .helpers import EvalContext - -__all__ = ["InternalEval"] - -log = logging.getLogger("rattlesnake.exts.admin_tools.internal_eval") - -CODEBLOCK_REGEX = re.compile(r"(^```(py(thon)?)?\n)|(```$)") - - -class InternalEval(commands.Cog): - """Top secret code evaluation for admins and owners.""" - - def __init__(self, bot: Rattlesnake): - self.bot = bot - self.locals = {} - - @staticmethod - def shorten_output( - output: str, - max_length: int = 1900, - placeholder: str = "\n[output truncated]" - ) -> str: - """ - Shorten the `output` so it's shorter than `max_length`. - There are three tactics for this, tried in the following order: - - Shorten the output on a line-by-line basis - - Shorten the output on any whitespace character - - Shorten the output solely on character count - """ - max_length = max_length - len(placeholder) - - shortened_output = [] - char_count = 0 - for line in output.split("\n"): - if char_count + len(line) > max_length: - break - shortened_output.append(line) - char_count += len(line) + 1 # account for (possible) line ending - - if shortened_output: - shortened_output.append(placeholder) - return "\n".join(shortened_output) - - shortened_output = textwrap.shorten(output, width=max_length, placeholder=placeholder) - - if shortened_output.strip() == placeholder.strip(): - # `textwrap` was unable to find whitespace to shorten on, so it has - # reduced the output to just the placeholder. Let's shorten based on - # characters instead. - shortened_output = output[:max_length] + placeholder - - return shortened_output - - async def _upload_output(self, output: str) -> typing.Optional[str]: - """Upload `internal eval` output to our pastebin and return the url.""" - try: - async with self.bot.http_session.post( - "https://paste.pythondiscord.com/documents", data=output, raise_for_status=True - ) as resp: - data = await resp.json() - - if "key" in data: - return f"https://paste.pythondiscord.com/{data['key']}" - except Exception: - # 400 (Bad Request) means there are too many characters - log.exception("Failed to upload `internal eval` output to paste service!") - - async def _send_output(self, ctx: commands.Context, output: str) -> None: - """Send the `internal eval` output to the command invocation context.""" - upload_message = "" - if len(output) >= 1980: - # The output is too long, let's truncate it for in-channel output and - # upload the complete output to the paste service. - url = await self._upload_output(output) - - if url: - upload_message = f"\nFull output here: {url}" - else: - upload_message = "\n:warning: Failed to upload full output!" - - output = self.shorten_output(output) - - await ctx.send(f"```py\n{output}```{upload_message}") - - async def _eval(self, ctx: commands.Context, code: str) -> None: - """Evaluate the `code` in the current evaluation context.""" - if code.startswith("exit"): - self.locals = {} - await ctx.send("The evaluation context was reset.") - return - - context_vars = { - "message": ctx.message, - "author": ctx.message.author, - "channel": ctx.channel, - "guild": ctx.guild, - "ctx": ctx, - "self": self, - "bot": self.bot, - "discord": discord, - } - - eval_context = EvalContext(context_vars, self.locals) - - log.trace("Preparing the evaluation by parsing the AST of the code") - error = eval_context.prepare_eval(code) - - if error: - log.trace("The code can't be evaluated due to an error") - await ctx.send(f"```py\n{error}\n```") - return - - log.trace("Evaluate the AST we've generated for the evaluation") - new_locals = await eval_context.run_eval() - - log.trace("Updating locals with those set during evaluation") - self.locals.update(new_locals) - - log.trace("Sending the formatted output back to the context") - await self._send_output(ctx, eval_context.format_output()) - - @commands.group(name='internal', aliases=('int',)) - @in_whitelist(roles=ADMIN_ROLES) - async def internal_group(self, ctx: commands.Context) -> None: - """Internal commands. Top secret!""" - if not ctx.invoked_subcommand: - await ctx.send_help(ctx.command) - - @internal_group.command(name='eval', aliases=('e',)) - @in_whitelist(roles=ADMIN_ROLES) - async def eval(self, ctx: commands.Context, *, code: str) -> None: - """Run eval in a REPL-like format.""" - code = CODEBLOCK_REGEX.sub("", code.strip()) - await self._eval(ctx, code) - - @internal_group.command(name='reset', aliases=("clear", "exit", "r", "c")) - @in_whitelist(roles=ADMIN_ROLES) - async def reset(self, ctx: commands.Context) -> None: - """Run eval in a REPL-like format.""" - self.locals = {} - await ctx.send("The evaluation context was reset.") -- cgit v1.2.3 From 13be6262e6048790062be9dd7daf178fb8b8d0e5 Mon Sep 17 00:00:00 2001 From: janine9vn Date: Sat, 10 Apr 2021 20:23:45 -0400 Subject: Update codeblock regex The snekbox implementation of the codeblock regex was incorporated. This now correctly parses the `code` and ``code`` markdown discord allows. You can also use multiple code blocks with text interrupting it and it will process the different code blocks as one continuous code block. --- bot/exts/internal_eval/_internal_eval.py | 37 +++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index f7a0946b..45bfbdc3 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -17,6 +17,23 @@ log = logging.getLogger("rattlesnake.exts.admin_tools.internal_eval") CODEBLOCK_REGEX = re.compile(r"(^```(py(thon)?)?\n)|(```$)") +FORMATTED_CODE_REGEX = re.compile( + r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block + r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) + r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P.*?)" # extract all code inside the markup + r"\s*" # any more whitespace before the end of the code markup + r"(?P=delim)", # match the exact same delimiter from the start again + re.DOTALL | re.IGNORECASE # "." also matches newlines, case insensitive +) + +RAW_CODE_REGEX = re.compile( + r"^(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P.*?)" # extract all the rest as code + r"\s*$", # any trailing whitespace until the end of the string + re.DOTALL # "." also matches newlines +) + class InternalEval(commands.Cog): """Top secret code evaluation for admins and owners.""" @@ -141,7 +158,25 @@ class InternalEval(commands.Cog): @with_role(Roles.admin) async def eval(self, ctx: commands.Context, *, code: str) -> None: """Run eval in a REPL-like format.""" - code = CODEBLOCK_REGEX.sub("", code.strip()) + + if match := list(FORMATTED_CODE_REGEX.finditer(code)): + blocks = [block for block in match if block.group("block")] + + if len(blocks) > 1: + code = '\n'.join(block.group("code") for block in blocks) + info = "several code blocks" + else: + match = match[0] if len(blocks) == 0 else blocks[0] + code, block, lang, delim = match.group("code", "block", "lang", "delim") + if block: + info = (f"'{lang}' highlighted" if lang else "plain") + " code block" + else: + info = f"{delim}-enclosed inline code" + else: + code = RAW_CODE_REGEX.fullmatch(code).group("code") + info = "unformatted or badly formatted code" + + code = textwrap.dedent(code) await self._eval(ctx, code) @internal_group.command(name='reset', aliases=("clear", "exit", "r", "c")) -- cgit v1.2.3 From e946b50c42412a08c823a89e087bd8c67c20a85b Mon Sep 17 00:00:00 2001 From: janine9vn Date: Sat, 10 Apr 2021 20:49:41 -0400 Subject: Removed rogue variable I'm better than this I swear. I can lint before I commit. Don't tell lemon. --- bot/exts/internal_eval/_internal_eval.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index 45bfbdc3..6f29a661 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -164,17 +164,12 @@ class InternalEval(commands.Cog): if len(blocks) > 1: code = '\n'.join(block.group("code") for block in blocks) - info = "several code blocks" else: match = match[0] if len(blocks) == 0 else blocks[0] code, block, lang, delim = match.group("code", "block", "lang", "delim") - if block: - info = (f"'{lang}' highlighted" if lang else "plain") + " code block" - else: - info = f"{delim}-enclosed inline code" + else: code = RAW_CODE_REGEX.fullmatch(code).group("code") - info = "unformatted or badly formatted code" code = textwrap.dedent(code) await self._eval(ctx, code) -- cgit v1.2.3 From 341908df150b6129055c970c7d2a8d36d76a4bfe Mon Sep 17 00:00:00 2001 From: janine9vn Date: Sat, 10 Apr 2021 21:07:48 -0400 Subject: Blind Fixes at Linting Both my pre-commit and flake8 runs are telling me everything is fine and it's all passed. Github actions is saying otherwise but isn't saying *where*. So here I am with useless linting commits. --- bot/exts/internal_eval/_helpers.py | 4 ++++ bot/exts/internal_eval/_internal_eval.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_helpers.py b/bot/exts/internal_eval/_helpers.py index 5c602e4d..a8ae5bef 100644 --- a/bot/exts/internal_eval/_helpers.py +++ b/bot/exts/internal_eval/_helpers.py @@ -74,6 +74,7 @@ def format_internal_eval_exception(exc_info: ExcInfo, code: str) -> str: class EvalContext: """ Represents the current `internal eval` context. + The context remembers names set during earlier runs of `internal eval`. To clear the context, use the `?internal clear` command. """ @@ -93,6 +94,7 @@ class EvalContext: def dependencies(self) -> typing.Dict[str, typing.Any]: """ Return a mapping of the dependencies for the wrapper function. + By using a property descriptor, the mapping can't be accidentally mutated during evaluation. This ensures the dependencies are always available. @@ -194,6 +196,7 @@ class WrapEvalCodeTree(ast.NodeTransformer): def visit_Pass(self, node: ast.Pass) -> typing.List[ast.AST]: # noqa: N802 """ Replace the `_ast.Pass` node in the wrapper function by the eval AST. + This method works on the assumption that there's a single `pass` statement in the wrapper function. """ @@ -211,6 +214,7 @@ class CaptureLastExpression(ast.NodeTransformer): def visit_Expr(self, node: ast.Expr) -> typing.Union[ast.Expr, ast.Assign]: # noqa: N802 """ Replace the Expr node that is last child node of Module with an assignment. + We use an assignment to capture the value of the last node, if it's a loose Expr node. Normally, the value of an Expr node is lost, meaning we don't get the output of such a last "loose" expression. By assigning it a name, we can diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index 6f29a661..ee438724 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -50,6 +50,7 @@ class InternalEval(commands.Cog): ) -> str: """ Shorten the `output` so it's shorter than `max_length`. + There are three tactics for this, tried in the following order: - Shorten the output on a line-by-line basis - Shorten the output on any whitespace character @@ -158,7 +159,6 @@ class InternalEval(commands.Cog): @with_role(Roles.admin) async def eval(self, ctx: commands.Context, *, code: str) -> None: """Run eval in a REPL-like format.""" - if match := list(FORMATTED_CODE_REGEX.finditer(code)): blocks = [block for block in match if block.group("block")] -- cgit v1.2.3 From 87e459836b8d3b0d624ec97fe293f994ba9c8c22 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sun, 11 Apr 2021 11:18:28 -0400 Subject: Correct prefix usage in a doctstring Corrects the prefix for the a command in the docstring to use Lancebot's prefix. Co-authored-by: Matteo Bertucci --- bot/exts/internal_eval/_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_helpers.py b/bot/exts/internal_eval/_helpers.py index a8ae5bef..8b991d98 100644 --- a/bot/exts/internal_eval/_helpers.py +++ b/bot/exts/internal_eval/_helpers.py @@ -76,7 +76,7 @@ class EvalContext: Represents the current `internal eval` context. The context remembers names set during earlier runs of `internal eval`. To - clear the context, use the `?internal clear` command. + clear the context, use the `.internal clear` command. """ def __init__(self, context_vars: Namespace, local_vars: Namespace) -> None: -- cgit v1.2.3 From 2cc2a2e618ed019de00054c768613e3e6ba2470c Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sun, 11 Apr 2021 11:36:56 -0400 Subject: Correct logger name Changed the initialization of the logging to pull dynamically so it can actually log correctly. --- bot/exts/internal_eval/_helpers.py | 2 +- bot/exts/internal_eval/_internal_eval.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_helpers.py b/bot/exts/internal_eval/_helpers.py index 8b991d98..bd36520d 100644 --- a/bot/exts/internal_eval/_helpers.py +++ b/bot/exts/internal_eval/_helpers.py @@ -11,7 +11,7 @@ import types import typing -log = logging.getLogger("rattlesnake.exts.admin_tools.internal_eval") +log = logging.getLogger(__name__) # A type alias to annotate the tuples returned from `sys.exc_info()` ExcInfo = typing.Tuple[typing.Type[Exception], Exception, types.TracebackType] diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index ee438724..198c1312 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -13,7 +13,7 @@ from ._helpers import EvalContext __all__ = ["InternalEval"] -log = logging.getLogger("rattlesnake.exts.admin_tools.internal_eval") +log = logging.getLogger(__name__) CODEBLOCK_REGEX = re.compile(r"(^```(py(thon)?)?\n)|(```$)") -- cgit v1.2.3 From fa67eebb08ec9d71d94cdeaf757cc84f33053691 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sun, 11 Apr 2021 13:58:39 -0400 Subject: Remove unused codeblock regex With the regex sufficiently stolen from snekbox and confirmed to work, the original codeblock regex has been removed. --- bot/exts/internal_eval/_internal_eval.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index 198c1312..4746c6c9 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -15,8 +15,6 @@ __all__ = ["InternalEval"] log = logging.getLogger(__name__) -CODEBLOCK_REGEX = re.compile(r"(^```(py(thon)?)?\n)|(```$)") - FORMATTED_CODE_REGEX = re.compile( r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) -- cgit v1.2.3 From e4829ffe61d8eda83ff89e7b86de35fe930bb793 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sun, 11 Apr 2021 14:06:39 -0400 Subject: Update docstring for reset Updated the docstring for `reset` to provide accurate information as to what the command does. --- bot/exts/internal_eval/_internal_eval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index 4746c6c9..8e474b7d 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -175,6 +175,6 @@ class InternalEval(commands.Cog): @internal_group.command(name='reset', aliases=("clear", "exit", "r", "c")) @with_role(Roles.admin) async def reset(self, ctx: commands.Context) -> None: - """Run eval in a REPL-like format.""" + """Reset the context and locals of the eval session.""" self.locals = {} await ctx.send("The evaluation context was reset.") -- cgit v1.2.3 From af97f53e2db9494dcf934d8b646b7d65c6713924 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sun, 11 Apr 2021 14:12:48 -0400 Subject: Change command help format `.int` with nothing else now uses the `invoke_help_command()` utility that formats the help command much more nicely than the default version --- bot/exts/internal_eval/_internal_eval.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index 8e474b7d..06626b69 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -9,6 +9,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Roles from bot.utils.decorators import with_role +from bot.utils.extensions import invoke_help_command from ._helpers import EvalContext __all__ = ["InternalEval"] @@ -151,7 +152,7 @@ class InternalEval(commands.Cog): async def internal_group(self, ctx: commands.Context) -> None: """Internal commands. Top secret!""" if not ctx.invoked_subcommand: - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) @internal_group.command(name='eval', aliases=('e',)) @with_role(Roles.admin) -- cgit v1.2.3 From 652f428347d2108d6c70df28c8c8130545ab9029 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sun, 11 Apr 2021 14:16:43 -0400 Subject: Ensure output will be robust for discord markdown Added in an extra `\n` at the end of the output. Sometimes discord won't properly format the codeblock in the triple ` is not on a newline. This changes ensures that it should. --- bot/exts/internal_eval/_internal_eval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index 06626b69..a62a7899 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -108,7 +108,7 @@ class InternalEval(commands.Cog): output = self.shorten_output(output) - await ctx.send(f"```py\n{output}```{upload_message}") + await ctx.send(f"```py\n{output}\n```{upload_message}") async def _eval(self, ctx: commands.Context, code: str) -> None: """Evaluate the `code` in the current evaluation context.""" -- cgit v1.2.3 From c45e26621f9ea4e6209a33541f5db996e3279ea0 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Mon, 12 Apr 2021 18:20:28 -0400 Subject: Add constants for common string filenames Added a constant for the same filenames used in several locations. Because the now-a-constant string is used in several locations this will allow for it to be updated more easily down the line. --- bot/exts/internal_eval/_helpers.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_helpers.py b/bot/exts/internal_eval/_helpers.py index bd36520d..3a50b9f3 100644 --- a/bot/exts/internal_eval/_helpers.py +++ b/bot/exts/internal_eval/_helpers.py @@ -41,6 +41,8 @@ async def _eval_wrapper_function(): _eval_context.locals = locals() _eval_context.function = _eval_wrapper_function """ +INTERNAL_EVAL_FRAMENAME = "" +EVAL_WRAPPER_FUNCTION_FRAMENAME = "_eval_wrapper_function" def format_internal_eval_exception(exc_info: ExcInfo, code: str) -> str: @@ -51,11 +53,11 @@ def format_internal_eval_exception(exc_info: ExcInfo, code: str) -> str: output = ["Traceback (most recent call last):"] for frame in stack_summary: - if frame.filename == "": + if frame.filename == INTERNAL_EVAL_FRAMENAME: line = code[frame.lineno - 1].lstrip() - if frame.name == "_eval_wrapper_function": - name = "" + if frame.name == EVAL_WRAPPER_FUNCTION_FRAMENAME: + name = INTERNAL_EVAL_FRAMENAME else: name = frame.name else: @@ -128,7 +130,7 @@ class EvalContext: return "[No code detected]" try: - code_tree = ast.parse(code, filename="") + code_tree = ast.parse(code, filename=INTERNAL_EVAL_FRAMENAME) except SyntaxError: log.debug("Got a SyntaxError while parsing the eval code") return "".join(traceback.format_exception(*sys.exc_info(), limit=0)) @@ -145,7 +147,7 @@ class EvalContext: async def run_eval(self) -> Namespace: """Run the evaluation and return the updated locals.""" log.trace("Compiling the AST to bytecode using `exec` mode") - compiled_code = compile(self.eval_tree, filename="", mode="exec") + compiled_code = compile(self.eval_tree, filename=INTERNAL_EVAL_FRAMENAME, mode="exec") log.trace("Executing the compiled code with the desired namespace environment") exec(compiled_code, self.locals) # noqa: B102,S102 @@ -186,7 +188,7 @@ class WrapEvalCodeTree(ast.NodeTransformer): self.eval_code_tree = eval_code_tree # To avoid mutable aliasing, parse the WRAPPER_FUNC for each wrapping - self.wrapper = ast.parse(EVAL_WRAPPER, filename="") + self.wrapper = ast.parse(EVAL_WRAPPER, filename=INTERNAL_EVAL_FRAMENAME) def wrap(self) -> ast.AST: """Wrap the tree of the code by the tree of the wrapper function.""" -- cgit v1.2.3 From c9a3cdf1e71525c307ee3385a375724a861a7b74 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Mon, 12 Apr 2021 18:37:01 -0400 Subject: Remove check for `exit` to reset context This commit removes the `exit` check if someone were to use this: `.int e exit` to clear the context. The check would prevent `.int e exit()` from restarting the bot container. With the `.int reset` and `.int exit` ability to clear the context the check for `exit` to clear the context isn't necessary. --- bot/exts/internal_eval/_internal_eval.py | 5 ----- 1 file changed, 5 deletions(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index a62a7899..757a2a1e 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -112,11 +112,6 @@ class InternalEval(commands.Cog): async def _eval(self, ctx: commands.Context, code: str) -> None: """Evaluate the `code` in the current evaluation context.""" - if code.startswith("exit"): - self.locals = {} - await ctx.send("The evaluation context was reset.") - return - context_vars = { "message": ctx.message, "author": ctx.message.author, -- cgit v1.2.3 From a6cc40ff3b323dff112d7f8c339e124f3a6d9980 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Tue, 4 May 2021 12:57:03 -0400 Subject: chore: Prefer double quotes over single quotes --- bot/bot.py | 2 +- bot/exts/christmas/advent_of_code/_helpers.py | 8 +- bot/exts/christmas/hanukkah_embed.py | 28 ++--- bot/exts/easter/april_fools_vids.py | 2 +- bot/exts/easter/bunny_name_generator.py | 16 +-- bot/exts/easter/earth_photos.py | 2 +- bot/exts/easter/egg_facts.py | 2 +- bot/exts/easter/egghead_quiz.py | 12 +- bot/exts/easter/save_the_planet.py | 4 +- bot/exts/easter/traditions.py | 2 +- bot/exts/evergreen/cheatsheet.py | 4 +- bot/exts/evergreen/connect_four.py | 6 +- bot/exts/evergreen/conversationstarters.py | 12 +- bot/exts/evergreen/emoji.py | 6 +- bot/exts/evergreen/error_handler.py | 4 +- bot/exts/evergreen/game.py | 4 +- bot/exts/evergreen/githubinfo.py | 42 +++---- bot/exts/evergreen/help.py | 74 ++++++------- bot/exts/evergreen/issues.py | 4 +- bot/exts/evergreen/minesweeper.py | 6 +- bot/exts/evergreen/movie.py | 20 ++-- bot/exts/evergreen/pythonfacts.py | 8 +- bot/exts/evergreen/recommend_game.py | 10 +- bot/exts/evergreen/reddit.py | 20 ++-- bot/exts/evergreen/snakes/_converter.py | 12 +- bot/exts/evergreen/snakes/_snakes_cog.py | 152 +++++++++++++------------- bot/exts/evergreen/snakes/_utils.py | 34 +++--- bot/exts/evergreen/source.py | 2 +- bot/exts/evergreen/speedrun.py | 2 +- bot/exts/evergreen/status_codes.py | 16 +-- bot/exts/evergreen/tic_tac_toe.py | 2 +- bot/exts/evergreen/trivia_quiz.py | 2 +- bot/exts/evergreen/wikipedia.py | 10 +- bot/exts/evergreen/wolfram.py | 4 +- bot/exts/evergreen/xkcd.py | 4 +- bot/exts/halloween/8ball.py | 2 +- bot/exts/halloween/candy_collection.py | 18 +-- bot/exts/halloween/hacktober-issue-finder.py | 2 +- bot/exts/halloween/hacktoberstats.py | 8 +- bot/exts/halloween/halloweenify.py | 4 +- bot/exts/halloween/monsterbio.py | 2 +- bot/exts/halloween/monstersurvey.py | 84 +++++++------- bot/exts/halloween/scarymovie.py | 22 ++-- bot/exts/halloween/spookygif.py | 6 +- bot/exts/halloween/spookynamerate.py | 2 +- bot/exts/halloween/spookyrating.py | 10 +- bot/exts/halloween/spookyreact.py | 14 +-- bot/exts/internal_eval/_internal_eval.py | 8 +- bot/exts/valentines/be_my_valentine.py | 34 +++--- bot/exts/valentines/myvalenstate.py | 10 +- bot/exts/valentines/pickuplines.py | 8 +- bot/exts/valentines/savethedate.py | 2 +- bot/exts/valentines/valentine_zodiac.py | 32 +++--- bot/exts/valentines/whoisvalentine.py | 12 +- bot/utils/__init__.py | 14 +-- bot/utils/checks.py | 4 +- bot/utils/decorators.py | 2 +- bot/utils/extensions.py | 4 +- bot/utils/halloween/spookifications.py | 10 +- bot/utils/pagination.py | 8 +- 60 files changed, 430 insertions(+), 430 deletions(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/bot.py b/bot/bot.py index 7e495940..b8de97aa 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -101,7 +101,7 @@ class Bot(commands.Bot): all_channels_ids = [channel.id for channel in self.get_all_channels()] for name, channel_id in vars(constants.Channels).items(): - if name.startswith('_'): + if name.startswith("_"): continue if channel_id not in all_channels_ids: log.error(f'Channel "{name}" with ID {channel_id} missing') diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index a16a4871..f4a258c0 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -108,7 +108,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: # star view. We need that per star view to compute rank scores per star. for member in raw_leaderboard_data.values(): name = member["name"] if member["name"] else f"Anonymous #{member['id']}" - member_id = member['id'] + member_id = member["id"] leaderboard[member_id] = {"name": name, "score": 0, "star_1": 0, "star_2": 0} # Iterate over all days for this participant @@ -119,7 +119,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: leaderboard[member_id][f"star_{star}"] += 1 # Record completion datetime for this participant for this day/star - completion_time = datetime.datetime.fromtimestamp(int(data['get_star_ts'])) + completion_time = datetime.datetime.fromtimestamp(int(data["get_star_ts"])) star_results[(day, star)].append( StarResult(member_id=member_id, completion_time=completion_time) ) @@ -133,7 +133,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: if day in AdventOfCode.ignored_days: continue - sorted_result = sorted(results, key=operator.attrgetter('completion_time')) + sorted_result = sorted(results, key=operator.attrgetter("completion_time")) for rank, star_result in enumerate(sorted_result): leaderboard[star_result.member_id]["score"] += max_score - rank @@ -307,7 +307,7 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict: def get_summary_embed(leaderboard: dict) -> discord.Embed: """Get an embed with the current summary stats of the leaderboard.""" - leaderboard_url = leaderboard['full_leaderboard_url'] + leaderboard_url = leaderboard["full_leaderboard_url"] refresh_minutes = AdventOfCode.leaderboard_cache_expiry_seconds // 60 aoc_embed = discord.Embed( diff --git a/bot/exts/christmas/hanukkah_embed.py b/bot/exts/christmas/hanukkah_embed.py index cd8a9192..32002f76 100644 --- a/bot/exts/christmas/hanukkah_embed.py +++ b/bot/exts/christmas/hanukkah_embed.py @@ -28,15 +28,15 @@ class HanukkahEmbed(commands.Cog): hanukkah_dates = [] async with self.bot.http_session.get(self.url) as response: json_data = await response.json() - festivals = json_data['items'] + festivals = json_data["items"] for festival in festivals: - if festival['title'].startswith('Chanukah'): - date = festival['date'] + if festival["title"].startswith("Chanukah"): + date = festival["date"] hanukkah_dates.append(date) return hanukkah_dates @in_month(Month.DECEMBER) - @commands.command(name='hanukkah', aliases=['chanukah']) + @commands.command(name="hanukkah", aliases=["chanukah"]) async def hanukkah_festival(self, ctx: commands.Context) -> None: """Tells you about the Hanukkah Festivaltime of festival, festival day, etc).""" hanukkah_dates = await self.get_hanukkah_dates() @@ -56,7 +56,7 @@ class HanukkahEmbed(commands.Cog): month = str(today.month) year = str(today.year) embed = Embed() - embed.title = 'Hanukkah' + embed.title = "Hanukkah" embed.colour = Colours.blue if day in self.hanukkah_days and month in self.hanukkah_months and year in self.hanukkah_years: if int(day) == hanukkah_start_day: @@ -69,13 +69,13 @@ class HanukkahEmbed(commands.Cog): await ctx.send(embed=embed) return elif hours > hanukkah_start_hour: - embed.description = (f'It is the starting day of Hanukkah ! ' - f'Its been {hours-hanukkah_start_hour} hours hanukkah started !') + embed.description = (f"It is the starting day of Hanukkah ! " + f"Its been {hours-hanukkah_start_hour} hours hanukkah started !") await ctx.send(embed=embed) return festival_day = self.hanukkah_days.index(day) - number_suffixes = ['st', 'nd', 'rd', 'th'] - suffix = '' + number_suffixes = ["st", "nd", "rd", "th"] + suffix = "" if int(festival_day) == 1: suffix = number_suffixes[0] if int(festival_day) == 2: @@ -84,19 +84,19 @@ class HanukkahEmbed(commands.Cog): suffix = number_suffixes[2] if int(festival_day) > 3: suffix = number_suffixes[3] - message = '' + message = "" for _ in range(1, festival_day + 1): - message += ':menorah:' - embed.description = f'It is the {festival_day}{suffix} day of Hanukkah ! \n {message}' + message += ":menorah:" + embed.description = f"It is the {festival_day}{suffix} day of Hanukkah ! \n {message}" await ctx.send(embed=embed) else: if today < hanukkah_start: - festival_starting_month = hanukkah_start.strftime('%B') + festival_starting_month = hanukkah_start.strftime("%B") embed.description = (f"Hanukkah has not started yet. " f"Hanukkah will start at sundown on {hanukkah_start_day}th " f"of {festival_starting_month}.") else: - festival_end_month = hanukkah_end.strftime('%B') + festival_end_month = hanukkah_end.strftime("%B") embed.description = (f"Looks like you missed Hanukkah !" f"Hanukkah ended on {hanukkah_end_day}th of {festival_end_month}.") diff --git a/bot/exts/easter/april_fools_vids.py b/bot/exts/easter/april_fools_vids.py index 84aa2913..3ce1f72a 100644 --- a/bot/exts/easter/april_fools_vids.py +++ b/bot/exts/easter/april_fools_vids.py @@ -15,7 +15,7 @@ with open("bot/resources/easter/april_fools_vids.json", encoding="utf-8") as f: class AprilFoolVideos(commands.Cog): """A cog for April Fools' that gets a random April Fools' video from Youtube.""" - @commands.command(name='fool') + @commands.command(name="fool") async def april_fools(self, ctx: commands.Context) -> None: """Get a random April Fools' video from Youtube.""" video = random.choice(ALL_VIDS) diff --git a/bot/exts/easter/bunny_name_generator.py b/bot/exts/easter/bunny_name_generator.py index 6d9e2a57..23f85226 100644 --- a/bot/exts/easter/bunny_name_generator.py +++ b/bot/exts/easter/bunny_name_generator.py @@ -20,7 +20,7 @@ class BunnyNameGenerator(commands.Cog): def find_separators(self, displayname: str) -> Union[List[str], None]: """Check if Discord name contains spaces so we can bunnify an individual word in the name.""" - new_name = re.split(r'[_.\s]', displayname) + new_name = re.split(r"[_.\s]", displayname) if displayname not in new_name: return new_name @@ -33,11 +33,11 @@ class BunnyNameGenerator(commands.Cog): Only the most recently matched pattern will apply the changes. """ expressions = [ - (r'a.+y', 'patchy'), - (r'e.+y', 'ears'), - (r'i.+y', 'ditsy'), - (r'o.+y', 'oofy'), - (r'u.+y', 'uffy'), + ("a.+y", "patchy"), + ("e.+y", "ears"), + ("i.+y", "ditsy"), + ("o.+y", "oofy"), + ("u.+y", "uffy"), ] for exp, vowel_sub in expressions: @@ -47,7 +47,7 @@ class BunnyNameGenerator(commands.Cog): def append_name(self, displayname: str) -> str: """Adds a suffix to the end of the Discord name.""" - extensions = ['foot', 'ear', 'nose', 'tail'] + extensions = ["foot", "ear", "nose", "tail"] suffix = random.choice(extensions) appended_name = displayname + suffix @@ -74,7 +74,7 @@ class BunnyNameGenerator(commands.Cog): unmatched_name = self.append_name(username) if spaces_in_name is not None: - replacements = ['Cotton', 'Fluff', 'Floof' 'Bounce', 'Snuffle', 'Nibble', 'Cuddle', 'Velvetpaw', 'Carrot'] + replacements = ["Cotton", "Fluff", "Floof" "Bounce", "Snuffle", "Nibble", "Cuddle", "Velvetpaw", "Carrot"] word_to_replace = random.choice(spaces_in_name) substitute = random.choice(replacements) bunnified_name = username.replace(word_to_replace, substitute) diff --git a/bot/exts/easter/earth_photos.py b/bot/exts/easter/earth_photos.py index d7e7ccc3..0e82e99a 100644 --- a/bot/exts/easter/earth_photos.py +++ b/bot/exts/easter/earth_photos.py @@ -21,7 +21,7 @@ class EarthPhotos(commands.Cog): """Returns a random photo of earth, sourced from Unsplash.""" async with ctx.typing(): async with self.bot.http_session.get( - 'https://api.unsplash.com/photos/random', + "https://api.unsplash.com/photos/random", params={"query": "planet_earth", "client_id": Tokens.unsplash_access_key} ) as r: jsondata = await r.json() diff --git a/bot/exts/easter/egg_facts.py b/bot/exts/easter/egg_facts.py index 78a5e592..8c93ca7b 100644 --- a/bot/exts/easter/egg_facts.py +++ b/bot/exts/easter/egg_facts.py @@ -41,7 +41,7 @@ class EasterFacts(commands.Cog): channel = self.bot.get_channel(Channels.community_bot_commands) await channel.send(embed=self.make_embed()) - @commands.command(name='eggfact', aliases=['fact']) + @commands.command(name="eggfact", aliases=["fact"]) async def easter_facts(self, ctx: commands.Context) -> None: """Get easter egg facts.""" embed = self.make_embed() diff --git a/bot/exts/easter/egghead_quiz.py b/bot/exts/easter/egghead_quiz.py index e950bc2e..59c1f6f8 100644 --- a/bot/exts/easter/egghead_quiz.py +++ b/bot/exts/easter/egghead_quiz.py @@ -18,12 +18,12 @@ with open(Path("bot/resources/easter/egghead_questions.json"), "r", encoding="ut EMOJIS = [ - '\U0001f1e6', '\U0001f1e7', '\U0001f1e8', '\U0001f1e9', '\U0001f1ea', - '\U0001f1eb', '\U0001f1ec', '\U0001f1ed', '\U0001f1ee', '\U0001f1ef', - '\U0001f1f0', '\U0001f1f1', '\U0001f1f2', '\U0001f1f3', '\U0001f1f4', - '\U0001f1f5', '\U0001f1f6', '\U0001f1f7', '\U0001f1f8', '\U0001f1f9', - '\U0001f1fa', '\U0001f1fb', '\U0001f1fc', '\U0001f1fd', '\U0001f1fe', - '\U0001f1ff' + "\U0001f1e6", "\U0001f1e7", "\U0001f1e8", "\U0001f1e9", "\U0001f1ea", + "\U0001f1eb", "\U0001f1ec", "\U0001f1ed", "\U0001f1ee", "\U0001f1ef", + "\U0001f1f0", "\U0001f1f1", "\U0001f1f2", "\U0001f1f3", "\U0001f1f4", + "\U0001f1f5", "\U0001f1f6", "\U0001f1f7", "\U0001f1f8", "\U0001f1f9", + "\U0001f1fa", "\U0001f1fb", "\U0001f1fc", "\U0001f1fd", "\U0001f1fe", + "\U0001f1ff" ] # Regional Indicators A-Z (used for voting) TIMELIMIT = 30 diff --git a/bot/exts/easter/save_the_planet.py b/bot/exts/easter/save_the_planet.py index db9d3498..444bb030 100644 --- a/bot/exts/easter/save_the_planet.py +++ b/bot/exts/easter/save_the_planet.py @@ -8,14 +8,14 @@ from bot.bot import Bot from bot.utils.randomization import RandomCycle -with Path("bot/resources/easter/save_the_planet.json").open('r', encoding='utf8') as f: +with Path("bot/resources/easter/save_the_planet.json").open("r", encoding="utf8") as f: EMBED_DATA = RandomCycle(json.load(f)) class SaveThePlanet(commands.Cog): """A cog that teaches users how they can help our planet.""" - @commands.command(aliases=('savetheearth', 'saveplanet', 'saveearth')) + @commands.command(aliases=("savetheearth", "saveplanet", "saveearth")) async def savetheplanet(self, ctx: commands.Context) -> None: """Responds with a random tip on how to be eco-friendly and help our planet.""" return_embed = Embed.from_dict(next(EMBED_DATA)) diff --git a/bot/exts/easter/traditions.py b/bot/exts/easter/traditions.py index 19e69b98..cb70f2d0 100644 --- a/bot/exts/easter/traditions.py +++ b/bot/exts/easter/traditions.py @@ -16,7 +16,7 @@ with open(Path("bot/resources/easter/traditions.json"), "r", encoding="utf8") as class Traditions(commands.Cog): """A cog which allows users to get a random easter tradition or custom from a random country.""" - @commands.command(aliases=('eastercustoms',)) + @commands.command(aliases=("eastercustoms",)) async def easter_tradition(self, ctx: commands.Context) -> None: """Responds with a random tradition or custom.""" random_country = random.choice(list(traditions)) diff --git a/bot/exts/evergreen/cheatsheet.py b/bot/exts/evergreen/cheatsheet.py index 57c6d0b0..86fae167 100644 --- a/bot/exts/evergreen/cheatsheet.py +++ b/bot/exts/evergreen/cheatsheet.py @@ -24,11 +24,11 @@ Unknown cheat sheet. Please try to reformulate your query. If the problem persists send a message in <#{Channels.dev_contrib}> """ -URL = 'https://cheat.sh/python/{search}' +URL = "https://cheat.sh/python/{search}" ESCAPE_TT = str.maketrans({"`": "\\`"}) ANSI_RE = re.compile(r"\x1b\[.*?m") # We need to pass headers as curl otherwise it would default to aiohttp which would return raw html. -HEADERS = {'User-Agent': 'curl/7.68.0'} +HEADERS = {"User-Agent": "curl/7.68.0"} class CheatSheet(commands.Cog): diff --git a/bot/exts/evergreen/connect_four.py b/bot/exts/evergreen/connect_four.py index df2a913a..929a15d8 100644 --- a/bot/exts/evergreen/connect_four.py +++ b/bot/exts/evergreen/connect_four.py @@ -55,8 +55,8 @@ class Game: async def print_grid(self) -> None: """Formats and outputs the Connect Four grid to the channel.""" title = ( - f'Connect 4: {self.player1.display_name}' - f' VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}' + f"Connect 4: {self.player1.display_name}" + f" VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}" ) rows = [" ".join(self.tokens[s] for s in row) for row in self.grid] @@ -67,7 +67,7 @@ class Game: if self.message: await self.message.edit(embed=embed) else: - self.message = await self.channel.send(content='Loading...') + self.message = await self.channel.send(content="Loading...") for emoji in self.unicode_numbers: await self.message.add_reaction(emoji) await self.message.add_reaction(CROSS_EMOJI) diff --git a/bot/exts/evergreen/conversationstarters.py b/bot/exts/evergreen/conversationstarters.py index 54fea0b3..4fe8c47c 100644 --- a/bot/exts/evergreen/conversationstarters.py +++ b/bot/exts/evergreen/conversationstarters.py @@ -9,7 +9,7 @@ from bot.constants import WHITELISTED_CHANNELS from bot.utils.decorators import whitelist_override from bot.utils.randomization import RandomCycle -SUGGESTION_FORM = 'https://forms.gle/zw6kkJqv8U43Nfjg9' +SUGGESTION_FORM = "https://forms.gle/zw6kkJqv8U43Nfjg9" with Path("bot/resources/evergreen/starter.yaml").open("r", encoding="utf8") as f: STARTERS = yaml.load(f, Loader=yaml.FullLoader) @@ -25,9 +25,9 @@ with Path("bot/resources/evergreen/py_topics.yaml").open("r", encoding="utf8") a ALL_ALLOWED_CHANNELS = list(PY_TOPICS.keys()) + list(WHITELISTED_CHANNELS) # Putting all topics into one dictionary and shuffling lists to reduce same-topic repetitions. -ALL_TOPICS = {'default': STARTERS, **PY_TOPICS} +ALL_TOPICS = {"default": STARTERS, **PY_TOPICS} TOPICS = { - channel: RandomCycle(topics or ['No topics found for this channel.']) + channel: RandomCycle(topics or ["No topics found for this channel."]) for channel, topics in ALL_TOPICS.items() } @@ -46,7 +46,7 @@ class ConvoStarters(commands.Cog): Otherwise, a random conversation topic will be received by the user. """ # No matter what, the form will be shown. - embed = Embed(description=f'Suggest more topics [here]({SUGGESTION_FORM})!', color=Color.blurple()) + embed = Embed(description=f"Suggest more topics [here]({SUGGESTION_FORM})!", color=Color.blurple()) try: # Fetching topics. @@ -54,11 +54,11 @@ class ConvoStarters(commands.Cog): # If the channel isn't Python-related. except KeyError: - embed.title = f'**{next(TOPICS["default"])}**' + embed.title = f"**{next(TOPICS['default'])}**" # If the channel ID doesn't have any topics. else: - embed.title = f'**{next(channel_topics)}**' + embed.title = f"**{next(channel_topics)}**" finally: await ctx.send(embed=embed) diff --git a/bot/exts/evergreen/emoji.py b/bot/exts/evergreen/emoji.py index 8e540712..e7452a15 100644 --- a/bot/exts/evergreen/emoji.py +++ b/bot/exts/evergreen/emoji.py @@ -46,9 +46,9 @@ class Emojis(commands.Cog): else: emoji_info = f"There is **{len(category_emojis)}** emoji in the **{category_name}** category." if emoji_choice.animated: - msg.append(f' {emoji_info}') + msg.append(f" {emoji_info}") else: - msg.append(f'<:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}') + msg.append(f"<:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}") return embed, msg @staticmethod @@ -64,7 +64,7 @@ class Emojis(commands.Cog): for emoji in emojis: emoji_dict[emoji.name.split("_")[0]].append(emoji) - error_comp = ', '.join(emoji_dict) + error_comp = ", ".join(emoji_dict) msg.append(f"These are the valid emoji categories:\n```{error_comp}```") return embed, msg diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py index dabd0ab5..5cd8d28d 100644 --- a/bot/exts/evergreen/error_handler.py +++ b/bot/exts/evergreen/error_handler.py @@ -40,7 +40,7 @@ class CommandErrorHandler(commands.Cog): @commands.Cog.listener() async def on_command_error(self, ctx: commands.Context, error: commands.CommandError) -> None: """Activates when a command opens an error.""" - if getattr(error, 'handled', False): + if getattr(error, "handled", False): logging.debug(f"Command {ctx.command} had its error already handled locally; ignoring.") return @@ -49,7 +49,7 @@ class CommandErrorHandler(commands.Cog): parent_command = f"{ctx.command} " ctx = subctx - error = getattr(error, 'original', error) + error = getattr(error, "original", error) logging.debug( f"Error Encountered: {type(error).__name__} - {str(error)}, " f"Command: {ctx.command}, " diff --git a/bot/exts/evergreen/game.py b/bot/exts/evergreen/game.py index 24872e76..7abbadcd 100644 --- a/bot/exts/evergreen/game.py +++ b/bot/exts/evergreen/game.py @@ -176,7 +176,7 @@ class Games(Cog): "Invalid OAuth credentials. Unloading Games cog. " f"OAuth response message: {result['message']}" ) - self.bot.remove_cog('Games') + self.bot.remove_cog("Games") return @@ -260,7 +260,7 @@ class Games(Cog): display_possibilities = "`, `".join(p[1] for p in possibilities) await ctx.send( f"Invalid genre `{genre}`. " - f"{f'Maybe you meant `{display_possibilities}`?' if display_possibilities else ''}" + f"Maybe you meant `{display_possibilities}`?" if display_possibilities else '' ) return elif len(possibilities) == 1: diff --git a/bot/exts/evergreen/githubinfo.py b/bot/exts/evergreen/githubinfo.py index fd100a7c..24479c79 100644 --- a/bot/exts/evergreen/githubinfo.py +++ b/bot/exts/evergreen/githubinfo.py @@ -27,14 +27,14 @@ class GithubInfo(commands.Cog): async with self.bot.http_session.get(url) as r: return await r.json() - @commands.group(name='github', aliases=('gh', 'git')) + @commands.group(name="github", aliases=("gh", "git")) @commands.cooldown(1, 10, BucketType.user) async def github_group(self, ctx: commands.Context) -> None: """Commands for finding information related to GitHub.""" if ctx.invoked_subcommand is None: await invoke_help_command(ctx) - @github_group.command(name='user', aliases=('userinfo',)) + @github_group.command(name="user", aliases=("userinfo",)) async def github_user_info(self, ctx: commands.Context, username: str) -> None: """Fetches a user's GitHub information.""" async with ctx.typing(): @@ -51,31 +51,31 @@ class GithubInfo(commands.Cog): await ctx.send(embed=embed) return - org_data = await self.fetch_data(user_data['organizations_url']) + org_data = await self.fetch_data(user_data["organizations_url"]) orgs = [f"[{org['login']}](https://github.com/{org['login']})" for org in org_data] - orgs_to_add = ' | '.join(orgs) + orgs_to_add = " | ".join(orgs) - gists = user_data['public_gists'] + gists = user_data["public_gists"] # Forming blog link - if user_data['blog'].startswith("http"): # Blog link is complete - blog = user_data['blog'] - elif user_data['blog']: # Blog exists but the link is not complete + if user_data["blog"].startswith("http"): # Blog link is complete + blog = user_data["blog"] + elif user_data["blog"]: # Blog exists but the link is not complete blog = f"https://{user_data['blog']}" else: blog = "No website link available" embed = discord.Embed( title=f"`{user_data['login']}`'s GitHub profile info", - description=f"```{user_data['bio']}```\n" if user_data['bio'] is not None else "", + description=f"```{user_data['bio']}```\n" if user_data["bio"] is not None else "", colour=discord.Colour.blurple(), - url=user_data['html_url'], - timestamp=datetime.strptime(user_data['created_at'], "%Y-%m-%dT%H:%M:%SZ") + url=user_data["html_url"], + timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") ) - embed.set_thumbnail(url=user_data['avatar_url']) + embed.set_thumbnail(url=user_data["avatar_url"]) embed.set_footer(text="Account created at") - if user_data['type'] == "User": + if user_data["type"] == "User": embed.add_field( name="Followers", @@ -91,7 +91,7 @@ class GithubInfo(commands.Cog): value=f"[{user_data['public_repos']}]({user_data['html_url']}?tab=repositories)" ) - if user_data['type'] == "User": + if user_data["type"] == "User": embed.add_field(name="Gists", value=f"[{gists}](https://gist.github.com/{quote(username, safe='')})") embed.add_field( @@ -109,8 +109,8 @@ class GithubInfo(commands.Cog): The repository should look like `user/reponame` or `user reponame`. """ - repo = '/'.join(repo) - if repo.count('/') != 1: + repo = "/".join(repo) + if repo.count("/") != 1: embed = discord.Embed( title=random.choice(NEGATIVE_REPLIES), description="The repository should look like `user/reponame` or `user reponame`.", @@ -135,10 +135,10 @@ class GithubInfo(commands.Cog): return embed = discord.Embed( - title=repo_data['name'], + title=repo_data["name"], description=repo_data["description"], colour=discord.Colour.blurple(), - url=repo_data['html_url'] + url=repo_data["html_url"] ) # If it's a fork, then it will have a parent key @@ -148,7 +148,7 @@ class GithubInfo(commands.Cog): except KeyError: log.debug("Repository is not a fork.") - repo_owner = repo_data['owner'] + repo_owner = repo_data["owner"] embed.set_author( name=repo_owner["login"], @@ -156,8 +156,8 @@ class GithubInfo(commands.Cog): icon_url=repo_owner["avatar_url"] ) - repo_created_at = datetime.strptime(repo_data['created_at'], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y") - last_pushed = datetime.strptime(repo_data['pushed_at'], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y at %H:%M") + repo_created_at = datetime.strptime(repo_data["created_at"], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y") + last_pushed = datetime.strptime(repo_data["pushed_at"], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y at %H:%M") embed.set_footer( text=( diff --git a/bot/exts/evergreen/help.py b/bot/exts/evergreen/help.py index f557e42e..bfaf25f1 100644 --- a/bot/exts/evergreen/help.py +++ b/bot/exts/evergreen/help.py @@ -22,14 +22,14 @@ from bot.utils.pagination import ( DELETE_EMOJI = Emojis.trashcan REACTIONS = { - FIRST_EMOJI: 'first', - LEFT_EMOJI: 'back', - RIGHT_EMOJI: 'next', - LAST_EMOJI: 'end', - DELETE_EMOJI: 'stop', + FIRST_EMOJI: "first", + LEFT_EMOJI: "back", + RIGHT_EMOJI: "next", + LAST_EMOJI: "end", + DELETE_EMOJI: "stop", } -Cog = namedtuple('Cog', ['name', 'description', 'commands']) +Cog = namedtuple("Cog", ["name", "description", "commands"]) log = logging.getLogger(__name__) @@ -87,7 +87,7 @@ class HelpSession: # set the query details for the session if command: - query_str = ' '.join(command) + query_str = " ".join(command) self.query = self._get_query(query_str) self.description = self.query.description or self.query.help else: @@ -191,7 +191,7 @@ class HelpSession: self.reset_timeout() # Run relevant action method - action = getattr(self, f'do_{REACTIONS[emoji]}', None) + action = getattr(self, f"do_{REACTIONS[emoji]}", None) if action: await action() @@ -234,11 +234,11 @@ class HelpSession: if cmd.cog: try: if cmd.cog.category: - return f'**{cmd.cog.category}**' + return f"**{cmd.cog.category}**" except AttributeError: pass - return f'**{cmd.cog_name}**' + return f"**{cmd.cog_name}**" else: return "**\u200bNo Category:**" @@ -262,47 +262,47 @@ class HelpSession: # if default is not an empty string or None if show_default: - results.append(f'[{name}={param.default}]') + results.append(f"[{name}={param.default}]") else: - results.append(f'[{name}]') + results.append(f"[{name}]") # if variable length argument elif param.kind == param.VAR_POSITIONAL: - results.append(f'[{name}...]') + results.append(f"[{name}...]") # if required else: - results.append(f'<{name}>') + results.append(f"<{name}>") return f"{cmd.name} {' '.join(results)}" async def build_pages(self) -> None: """Builds the list of content pages to be paginated through in the help message, as a list of str.""" # Use LinePaginator to restrict embed line height - paginator = LinePaginator(prefix='', suffix='', max_lines=self._max_lines) + paginator = LinePaginator(prefix="", suffix="", max_lines=self._max_lines) prefix = constants.Client.prefix # show signature if query is a command if isinstance(self.query, commands.Command): signature = self._get_command_params(self.query) - parent = self.query.full_parent_name + ' ' if self.query.parent else '' - paginator.add_line(f'**```{prefix}{parent}{signature}```**') + parent = self.query.full_parent_name + " " if self.query.parent else "" + paginator.add_line(f"**```{prefix}{parent}{signature}```**") aliases = [f"`{alias}`" if not parent else f"`{parent} {alias}`" for alias in self.query.aliases] aliases += [f"`{alias}`" for alias in getattr(self.query, "root_aliases", ())] aliases = ", ".join(sorted(aliases)) if aliases: - paginator.add_line(f'**Can also use:** {aliases}\n') + paginator.add_line(f"**Can also use:** {aliases}\n") if not await self.query.can_run(self._ctx): - paginator.add_line('***You cannot run this command.***\n') + paginator.add_line("***You cannot run this command.***\n") if isinstance(self.query, Cog): - paginator.add_line(f'**{self.query.name}**') + paginator.add_line(f"**{self.query.name}**") if self.description: - paginator.add_line(f'*{self.description}*') + paginator.add_line(f"*{self.description}*") # list all children commands of the queried object if isinstance(self.query, (commands.GroupMixin, Cog)): @@ -319,13 +319,13 @@ class HelpSession: return if isinstance(self.query, Cog): - grouped = (('**Commands:**', self.query.commands),) + grouped = (("**Commands:**", self.query.commands),) elif isinstance(self.query, commands.Command): - grouped = (('**Subcommands:**', self.query.commands),) + grouped = (("**Subcommands:**", self.query.commands),) # don't show prefix for subcommands - prefix = '' + prefix = "" # otherwise sort and organise all commands into categories else: @@ -347,7 +347,7 @@ class HelpSession: continue # see if the user can run the command - strikeout = '' + strikeout = "" # Patch to make the !help command work outside of #bot-commands again # This probably needs a proper rewrite, but this will make it work in @@ -361,16 +361,16 @@ class HelpSession: # skip if we don't show commands they can't run if self._only_can_run: continue - strikeout = '~~' + strikeout = "~~" signature = self._get_command_params(command) info = f"{strikeout}**`{prefix}{signature}`**{strikeout}" # handle if the command has no docstring if command.short_doc: - cat_cmds.append(f'{info}\n*{command.short_doc}*') + cat_cmds.append(f"{info}\n*{command.short_doc}*") else: - cat_cmds.append(f'{info}\n*No details provided.*') + cat_cmds.append(f"{info}\n*No details provided.*") # state var for if the category should be added next print_cat = 1 @@ -379,7 +379,7 @@ class HelpSession: for details in cat_cmds: # keep details together, paginating early if it won't fit - lines_adding = len(details.split('\n')) + print_cat + lines_adding = len(details.split("\n")) + print_cat if paginator._linecount + lines_adding > self._max_lines: paginator._linecount = 0 new_page = True @@ -390,7 +390,7 @@ class HelpSession: if print_cat: if new_page: - paginator.add_line('') + paginator.add_line("") paginator.add_line(category) print_cat = 0 @@ -412,7 +412,7 @@ class HelpSession: page_count = len(self._pages) if page_count > 1: - embed.set_footer(text=f'Page {self._current_page+1} / {page_count}') + embed.set_footer(text=f"Page {self._current_page+1} / {page_count}") return embed @@ -496,7 +496,7 @@ class HelpSession: class Help(DiscordCog): """Custom Embed Pagination Help feature.""" - @commands.command('help') + @commands.command("help") async def new_help(self, ctx: Context, *commands) -> None: """Shows Command Help.""" try: @@ -507,8 +507,8 @@ class Help(DiscordCog): embed.title = str(error) if error.possible_matches: - matches = '\n'.join(error.possible_matches.keys()) - embed.description = f'**Did you mean:**\n`{matches}`' + matches = "\n".join(error.possible_matches.keys()) + embed.description = f"**Did you mean:**\n`{matches}`" await ctx.send(embed=embed) @@ -519,7 +519,7 @@ def unload(bot: Bot) -> None: This is run if the cog raises an exception on load, or if the extension is unloaded. """ - bot.remove_command('help') + bot.remove_command("help") bot.add_command(bot._old_help) @@ -534,8 +534,8 @@ def setup(bot: Bot) -> None: If an exception is raised during the loading of the cog, `unload` will be called in order to reinstate the original help command. """ - bot._old_help = bot.get_command('help') - bot.remove_command('help') + bot._old_help = bot.get_command("help") + bot.remove_command("help") try: bot.add_cog(Help()) diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index d7ee99c0..5bbc57c6 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -158,7 +158,7 @@ class Issues(commands.Cog): issue_url = json_data.get("html_url") - return IssueState(repository, number, issue_url, json_data.get('title', ''), emoji) + return IssueState(repository, number, issue_url, json_data.get("title", ""), emoji) @staticmethod def format_embed( @@ -177,7 +177,7 @@ class Issues(commands.Cog): resp = discord.Embed( colour=Colours.bright_green, - description='\n'.join(description_list) + description="\n".join(description_list) ) embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}" diff --git a/bot/exts/evergreen/minesweeper.py b/bot/exts/evergreen/minesweeper.py index d0cc28c5..f2c5e656 100644 --- a/bot/exts/evergreen/minesweeper.py +++ b/bot/exts/evergreen/minesweeper.py @@ -38,7 +38,7 @@ class CoordinateConverter(commands.Converter): async def convert(self, ctx: commands.Context, coordinate: str) -> typing.Tuple[int, int]: """Take in a coordinate string and turn it into an (x, y) tuple.""" if not 2 <= len(coordinate) <= 3: - raise commands.BadArgument('Invalid co-ordinate provided.') + raise commands.BadArgument("Invalid co-ordinate provided.") coordinate = coordinate.lower() if coordinate[0].isalpha(): @@ -51,7 +51,7 @@ class CoordinateConverter(commands.Converter): if not digit.isdigit(): raise commands.BadArgument - x = ord(letter) - ord('a') + x = ord(letter) - ord("a") y = int(digit) - 1 if (not 0 <= x <= 9) or (not 0 <= y <= 9): @@ -82,7 +82,7 @@ class Minesweeper(commands.Cog): def __init__(self, _bot: Bot) -> None: self.games: GamesDict = {} # Store the currently running games - @commands.group(name='minesweeper', aliases=('ms',), invoke_without_command=True) + @commands.group(name="minesweeper", aliases=("ms",), invoke_without_command=True) async def minesweeper_group(self, ctx: commands.Context) -> None: """Commands for Playing Minesweeper.""" await invoke_help_command(ctx) diff --git a/bot/exts/evergreen/movie.py b/bot/exts/evergreen/movie.py index 488e5142..e67f8d04 100644 --- a/bot/exts/evergreen/movie.py +++ b/bot/exts/evergreen/movie.py @@ -53,7 +53,7 @@ class Movie(Cog): def __init__(self, bot: Bot): self.http_session: ClientSession = bot.http_session - @group(name='movies', aliases=['movie'], invoke_without_command=True) + @group(name="movies", aliases=["movie"], invoke_without_command=True) async def movies(self, ctx: Context, genre: str = "", amount: int = 5) -> None: """ Get random movies by specifying genre. Also support amount parameter, that define how much movies will be shown. @@ -89,7 +89,7 @@ class Movie(Cog): # Get movies list from TMDB, check if results key in result. When not, raise error. movies = await self.get_movies_list(self.http_session, MovieGenres[genre].value, page) - if 'results' not in movies.keys(): + if "results" not in movies.keys(): err_msg = f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " \ f"{result['status_message']}." await ctx.send(err_msg) @@ -101,7 +101,7 @@ class Movie(Cog): await ImagePaginator.paginate(pages, ctx, embed) - @movies.command(name='genres', aliases=['genre', 'g']) + @movies.command(name="genres", aliases=["genre", "g"]) async def genres(self, ctx: Context) -> None: """Show all currently available genres for .movies command.""" await ctx.send(f"Current available genres: {', '.join('`' + genre.name + '`' for genre in MovieGenres)}") @@ -130,7 +130,7 @@ class Movie(Cog): pages = [] for i in range(amount): - movie_id = movies['results'][i]['id'] + movie_id = movies["results"][i]["id"] movie = await self.get_movie(client, movie_id) page, img = await self.create_page(movie) @@ -151,7 +151,7 @@ class Movie(Cog): # Add title + tagline (if not empty) text += f"**{movie['title']}**\n" - if movie['tagline']: + if movie["tagline"]: text += f"{movie['tagline']}\n\n" else: text += "\n" @@ -162,8 +162,8 @@ class Movie(Cog): text += "__**Production Information**__\n" - companies = movie['production_companies'] - countries = movie['production_countries'] + companies = movie["production_companies"] + countries = movie["production_countries"] text += f"**Made by:** {', '.join(company['name'] for company in companies)}\n" text += f"**Made in:** {', '.join(country['name'] for country in countries)}\n\n" @@ -173,8 +173,8 @@ class Movie(Cog): budget = f"{movie['budget']:,d}" if movie['budget'] else "?" revenue = f"{movie['revenue']:,d}" if movie['revenue'] else "?" - if movie['runtime'] is not None: - duration = divmod(movie['runtime'], 60) + if movie["runtime"] is not None: + duration = divmod(movie["runtime"], 60) else: duration = ("?", "?") @@ -182,7 +182,7 @@ class Movie(Cog): text += f"**Revenue:** ${revenue}\n" text += f"**Duration:** {f'{duration[0]} hour(s) {duration[1]} minute(s)'}\n\n" - text += movie['overview'] + text += movie["overview"] img = f"http://image.tmdb.org/t/p/w200{movie['poster_path']}" diff --git a/bot/exts/evergreen/pythonfacts.py b/bot/exts/evergreen/pythonfacts.py index 73234d55..2dc4996f 100644 --- a/bot/exts/evergreen/pythonfacts.py +++ b/bot/exts/evergreen/pythonfacts.py @@ -6,7 +6,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours -with open('bot/resources/evergreen/python_facts.txt') as file: +with open("bot/resources/evergreen/python_facts.txt") as file: FACTS = itertools.cycle(list(file)) COLORS = itertools.cycle([Colours.python_blue, Colours.python_yellow]) @@ -15,13 +15,13 @@ COLORS = itertools.cycle([Colours.python_blue, Colours.python_yellow]) class PythonFacts(commands.Cog): """Sends a random fun fact about Python.""" - @commands.command(name='pythonfact', aliases=['pyfact']) + @commands.command(name="pythonfact", aliases=["pyfact"]) async def get_python_fact(self, ctx: commands.Context) -> None: """Sends a Random fun fact about Python.""" - embed = discord.Embed(title='Python Facts', + embed = discord.Embed(title="Python Facts", description=next(FACTS), colour=next(COLORS)) - embed.add_field(name='Suggestions', + embed.add_field(name="Suggestions", value="Suggest more facts [here!](https://github.com/python-discord/meta/discussions/93)") await ctx.send(embed=embed) diff --git a/bot/exts/evergreen/recommend_game.py b/bot/exts/evergreen/recommend_game.py index be329f44..340a42d3 100644 --- a/bot/exts/evergreen/recommend_game.py +++ b/bot/exts/evergreen/recommend_game.py @@ -13,7 +13,7 @@ game_recs = [] # Populate the list `game_recs` with resource files for rec_path in Path("bot/resources/evergreen/game_recs").glob("*.json"): - with rec_path.open(encoding='utf8') as file: + with rec_path.open(encoding="utf8") as file: data = json.load(file) game_recs.append(data) shuffle(game_recs) @@ -26,7 +26,7 @@ class RecommendGame(commands.Cog): self.bot = bot self.index = 0 - @commands.command(name="recommendgame", aliases=['gamerec']) + @commands.command(name="recommendgame", aliases=["gamerec"]) async def recommend_game(self, ctx: commands.Context) -> None: """Sends an Embed of a random game recommendation.""" if self.index >= len(game_recs): @@ -35,14 +35,14 @@ class RecommendGame(commands.Cog): game = game_recs[self.index] self.index += 1 - author = self.bot.get_user(int(game['author'])) + author = self.bot.get_user(int(game["author"])) # Creating and formatting Embed embed = discord.Embed(color=discord.Colour.blue()) if author is not None: embed.set_author(name=author.name, icon_url=author.avatar_url) - embed.set_image(url=game['image']) - embed.add_field(name='Recommendation: ' + game['title'] + '\n' + game['link'], value=game['description']) + embed.set_image(url=game["image"]) + embed.add_field(name="Recommendation: " + game["title"] + "\n" + game["link"], value=game["description"]) await ctx.send(embed=embed) diff --git a/bot/exts/evergreen/reddit.py b/bot/exts/evergreen/reddit.py index 51a360b3..82af6ce9 100644 --- a/bot/exts/evergreen/reddit.py +++ b/bot/exts/evergreen/reddit.py @@ -21,18 +21,18 @@ class Reddit(commands.Cog): """Send a get request to the reddit API and get json response.""" session = self.bot.http_session params = { - 'limit': 50 + "limit": 50 } headers = { - 'User-Agent': 'Iceman' + "User-Agent": "Iceman" } async with session.get(url=url, params=params, headers=headers) as response: return await response.json() - @commands.command(name='reddit') + @commands.command(name="reddit") @commands.cooldown(1, 10, BucketType.user) - async def get_reddit(self, ctx: commands.Context, subreddit: str = 'python', sort: str = "hot") -> None: + async def get_reddit(self, ctx: commands.Context, subreddit: str = "python", sort: str = "hot") -> None: """ Fetch reddit posts by using this command. @@ -46,15 +46,15 @@ class Reddit(commands.Cog): await ctx.send(f"Invalid sorting: {sort}\nUsing default sorting: `Hot`") sort = "hot" - data = await self.fetch(f'https://www.reddit.com/r/{subreddit}/{sort}/.json') + data = await self.fetch(f"https://www.reddit.com/r/{subreddit}/{sort}/.json") try: posts = data["data"]["children"] except KeyError: - await ctx.send('Subreddit not found!') + await ctx.send("Subreddit not found!") return if not posts: - await ctx.send('No posts available!') + await ctx.send("No posts available!") return if posts[0]["data"]["over_18"] is True: @@ -106,12 +106,12 @@ class Reddit(commands.Cog): post_stats = f"{image_emoji} " image_url = post_url - votes = f'{upvote_emoji}{post["data"]["ups"]}' - comments = f'{comment_emoji}\u2002{ post["data"]["num_comments"]}' + votes = f"{upvote_emoji}{post['data']['ups']}" + comments = f"{comment_emoji}\u2002{ post['data']['num_comments']}" post_stats += ( f"\u2002{votes}\u2003" f"{comments}" - f'\u2003{user_emoji}\u2002{post["data"]["author"]}\n' + f"\u2003{user_emoji}\u2002{post['data']['author']}\n" ) embed_titles += f"{post_stats}\n" page_text = f"**[{post_title}]({post_url})**\n{post_stats}\n{post['data']['selftext'][0:200]}" diff --git a/bot/exts/evergreen/snakes/_converter.py b/bot/exts/evergreen/snakes/_converter.py index eee248cf..0ca10d6c 100644 --- a/bot/exts/evergreen/snakes/_converter.py +++ b/bot/exts/evergreen/snakes/_converter.py @@ -24,8 +24,8 @@ class Snake(Converter): await self.build_list() name = name.lower() - if name == 'python': - return 'Python (programming language)' + if name == "python": + return "Python (programming language)" def get_potential(iterable: Iterable, *, threshold: int = 80) -> List[str]: nonlocal name @@ -47,12 +47,12 @@ class Snake(Converter): if name.lower() in self.special_cases: return self.special_cases.get(name.lower(), name.lower()) - names = {snake['name']: snake['scientific'] for snake in self.snakes} + names = {snake["name"]: snake["scientific"] for snake in self.snakes} all_names = names.keys() | names.values() timeout = len(all_names) * (3 / 4) embed = discord.Embed( - title='Found multiple choices. Please choose the correct one.', colour=0x59982F) + title="Found multiple choices. Please choose the correct one.", colour=0x59982F) embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.avatar_url) name = await disambiguate(ctx, get_potential(all_names), timeout=timeout, embed=embed) @@ -70,7 +70,7 @@ class Snake(Converter): if cls.special_cases is None: with (SNAKE_RESOURCES / "special_snakes.json").open(encoding="utf8") as snakefile: special_cases = json.load(snakefile) - cls.special_cases = {snake['name'].lower(): snake for snake in special_cases} + cls.special_cases = {snake["name"].lower(): snake for snake in special_cases} @classmethod async def random(cls) -> str: @@ -81,5 +81,5 @@ class Snake(Converter): so I can get it from here. """ await cls.build_list() - names = [snake['scientific'] for snake in cls.snakes] + names = [snake["scientific"] for snake in cls.snakes] return random.choice(names) diff --git a/bot/exts/evergreen/snakes/_snakes_cog.py b/bot/exts/evergreen/snakes/_snakes_cog.py index c8633ce7..d95970da 100644 --- a/bot/exts/evergreen/snakes/_snakes_cog.py +++ b/bot/exts/evergreen/snakes/_snakes_cog.py @@ -143,8 +143,8 @@ class Snakes(Cog): https://github.com/python-discord/code-jam-1 """ - wiki_brief = re.compile(r'(.*?)(=+ (.*?) =+)', flags=re.DOTALL) - valid_image_extensions = ('gif', 'png', 'jpeg', 'jpg', 'webp') + wiki_brief = re.compile(r"(.*?)(=+ (.*?) =+)", flags=re.DOTALL) + valid_image_extensions = ("gif", "png", "jpeg", "jpg", "webp") def __init__(self, bot: Bot): self.active_sal = {} @@ -183,28 +183,28 @@ class Snakes(Cog): # Get the size of the snake icon, configure the height of the image box (yes, it changes) icon_width = 347 # Hardcoded, not much i can do about that icon_height = int((icon_width / snake.width) * snake.height) - frame_copies = icon_height // CARD['frame'].height + 1 + frame_copies = icon_height // CARD["frame"].height + 1 snake.thumbnail((icon_width, icon_height)) # Get the dimensions of the final image - main_height = icon_height + CARD['top'].height + CARD['bottom'].height - main_width = CARD['frame'].width + main_height = icon_height + CARD["top"].height + CARD["bottom"].height + main_width = CARD["frame"].width # Start creating the foreground foreground = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0)) - foreground.paste(CARD['top'], (0, 0)) + foreground.paste(CARD["top"], (0, 0)) # Generate the frame borders to the correct height for offset in range(frame_copies): - position = (0, CARD['top'].height + offset * CARD['frame'].height) - foreground.paste(CARD['frame'], position) + position = (0, CARD["top"].height + offset * CARD["frame"].height) + foreground.paste(CARD["frame"], position) # Add the image and bottom part of the image - foreground.paste(snake, (36, CARD['top'].height)) # Also hardcoded :( - foreground.paste(CARD['bottom'], (0, CARD['top'].height + icon_height)) + foreground.paste(snake, (36, CARD["top"].height)) # Also hardcoded :( + foreground.paste(CARD["bottom"], (0, CARD["top"].height + icon_height)) # Setup the background - back = random.choice(CARD['backs']) + back = random.choice(CARD["backs"]) back_copies = main_height // back.height + 1 full_image = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0)) @@ -216,11 +216,11 @@ class Snakes(Cog): full_image.paste(foreground, (0, 0), foreground) # Get the first two sentences of the info - description = '.'.join(content['info'].split(".")[:2]) + '.' + description = ".".join(content["info"].split(".")[:2]) + "." # Setup positioning variables margin = 36 - offset = CARD['top'].height + icon_height + margin + offset = CARD["top"].height + icon_height + margin # Create blank rectangle image which will be behind the text rectangle = Image.new( @@ -242,12 +242,12 @@ class Snakes(Cog): # Draw the text onto the final image draw = ImageDraw.Draw(full_image) for line in textwrap.wrap(description, 36): - draw.text([margin + 4, offset], line, font=CARD['font']) - offset += CARD['font'].getsize(line)[1] + draw.text([margin + 4, offset], line, font=CARD["font"]) + offset += CARD["font"].getsize(line)[1] # Get the image contents as a BufferIO object buffer = BytesIO() - full_image.save(buffer, 'PNG') + full_image.save(buffer, "PNG") buffer.seek(0) return buffer @@ -311,12 +311,12 @@ class Snakes(Cog): async with aiohttp.ClientSession() as session: params = { - 'format': 'json', - 'action': 'query', - 'list': 'search', - 'srsearch': name, - 'utf8': '', - 'srlimit': '1', + "format": "json", + "action": "query", + "list": "search", + "srsearch": name, + "utf8": "", + "srlimit": "1", } json = await self._fetch(session, URL, params=params) @@ -331,13 +331,13 @@ class Snakes(Cog): return None params = { - 'format': 'json', - 'action': 'query', - 'prop': 'extracts|images|info', - 'exlimit': 'max', - 'explaintext': '', - 'inprop': 'url', - 'pageids': pageid + "format": "json", + "action": "query", + "prop": "extracts|images|info", + "exlimit": "max", + "explaintext": "", + "inprop": "url", + "pageids": pageid } json = await self._fetch(session, URL, params=params) @@ -353,32 +353,32 @@ class Snakes(Cog): snake_info["error"] = True if snake_info["images"]: - i_url = 'https://commons.wikimedia.org/wiki/Special:FilePath/' + i_url = "https://commons.wikimedia.org/wiki/Special:FilePath/" image_list = [] map_list = [] thumb_list = [] # Wikipedia has arbitrary images that are not snakes banned = [ - 'Commons-logo.svg', - 'Red%20Pencil%20Icon.png', - 'distribution', - 'The%20Death%20of%20Cleopatra%20arthur.jpg', - 'Head%20of%20holotype', - 'locator', - 'Woma.png', - '-map.', - '.svg', - 'ange.', - 'Adder%20(PSF).png' + "Commons-logo.svg", + "Red%20Pencil%20Icon.png", + "distribution", + "The%20Death%20of%20Cleopatra%20arthur.jpg", + "Head%20of%20holotype", + "locator", + "Woma.png", + "-map.", + ".svg", + "ange.", + "Adder%20(PSF).png" ] for image in snake_info["images"]: # Images come in the format of `File:filename.extension` - file, sep, filename = image["title"].partition(':') + file, sep, filename = image["title"].partition(":") filename = filename.replace(" ", "%20") # Wikipedia returns good data! - if not filename.startswith('Map'): + if not filename.startswith("Map"): if any(ban in filename for ban in banned): pass else: @@ -392,7 +392,7 @@ class Snakes(Cog): snake_info["thumb_list"] = thumb_list snake_info["name"] = name - match = self.wiki_brief.match(snake_info['extract']) + match = self.wiki_brief.match(snake_info["extract"]) info = match.group(1) if match else None if info: @@ -438,13 +438,13 @@ class Snakes(Cog): # endregion # region: Commands - @group(name='snakes', aliases=('snake',), invoke_without_command=True) + @group(name="snakes", aliases=("snake",), invoke_without_command=True) async def snakes_group(self, ctx: Context) -> None: """Commands from our first code jam.""" await invoke_help_command(ctx) @bot_has_permissions(manage_messages=True) - @snakes_group.command(name='antidote') + @snakes_group.command(name="antidote") @locked() async def antidote_command(self, ctx: Context) -> None: """ @@ -586,7 +586,7 @@ class Snakes(Cog): log.debug("Ending pagination and removing all reactions...") await board_id.clear_reactions() - @snakes_group.command(name='draw') + @snakes_group.command(name="draw") async def draw_command(self, ctx: Context) -> None: """ Draws a random snek using Perlin noise. @@ -621,10 +621,10 @@ class Snakes(Cog): bg_color=bg_color ) png_bytes = utils.frame_to_png_bytes(image_frame) - file = File(png_bytes, filename='snek.png') + file = File(png_bytes, filename="snek.png") await ctx.send(file=file) - @snakes_group.command(name='get') + @snakes_group.command(name="get") @bot_has_permissions(manage_messages=True) @locked() async def get_command(self, ctx: Context, *, name: Snake = None) -> None: @@ -642,8 +642,8 @@ class Snakes(Cog): else: data = await self._get_snek(name) - if data.get('error'): - await ctx.send('Could not fetch data from Wikipedia.') + if data.get("error"): + await ctx.send("Could not fetch data from Wikipedia.") return description = data["info"] @@ -662,19 +662,19 @@ class Snakes(Cog): # Build and send the embed. embed = Embed( - title=data.get("title", data.get('name')), + title=data.get("title", data.get("name")), description=description, colour=0x59982F, ) - emoji = 'https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png' - image = next((url for url in data['image_list'] + emoji = "https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png" + image = next((url for url in data["image_list"] if url.endswith(self.valid_image_extensions)), emoji) embed.set_image(url=image) await ctx.send(embed=embed) - @snakes_group.command(name='guess', aliases=('identify',)) + @snakes_group.command(name="guess", aliases=("identify",)) @locked() async def guess_command(self, ctx: Context) -> None: """ @@ -694,11 +694,11 @@ class Snakes(Cog): data = await self._get_snek(snake) - image = next((url for url in data['image_list'] + image = next((url for url in data["image_list"] if url.endswith(self.valid_image_extensions)), None) embed = Embed( - title='Which of the following is the snake in the image?', + title="Which of the following is the snake in the image?", description="\n".join( f"{'ABCD'[snakes.index(snake)]}: {snake}" for snake in snakes), colour=SNAKE_COLOR @@ -709,7 +709,7 @@ class Snakes(Cog): options = {f"{'abcd'[snakes.index(snake)]}": snake for snake in snakes} await self._validate_answer(ctx, guess, answer, options) - @snakes_group.command(name='hatch') + @snakes_group.command(name="hatch") async def hatch_command(self, ctx: Context) -> None: """ Hatches your personal snake. @@ -740,7 +740,7 @@ class Snakes(Cog): await ctx.send(embed=my_snake_embed) - @snakes_group.command(name='movie') + @snakes_group.command(name="movie") async def movie_command(self, ctx: Context) -> None: """ Gets a random snake-related movie from TMDB. @@ -806,7 +806,7 @@ class Snakes(Cog): await ctx.send("An error occurred while fetching a snake-related movie!") raise err from None - @snakes_group.command(name='quiz') + @snakes_group.command(name="quiz") @locked() async def quiz_command(self, ctx: Context) -> None: """ @@ -832,7 +832,7 @@ class Snakes(Cog): quiz = await ctx.send("", embed=embed) await self._validate_answer(ctx, quiz, answer, options) - @snakes_group.command(name='name', aliases=('name_gen',)) + @snakes_group.command(name="name", aliases=("name_gen",)) async def name_command(self, ctx: Context, *, name: str = None) -> None: """ Snakifies a username. @@ -856,7 +856,7 @@ class Snakes(Cog): This was written by Iceman, and modified for inclusion into the bot by lemon. """ snake_name = await self._get_snake_name() - snake_name = snake_name['name'] + snake_name = snake_name["name"] snake_prefix = "" # Set aside every word in the snake name except the last. @@ -904,7 +904,7 @@ class Snakes(Cog): await ctx.send(embed=embed) return - @snakes_group.command(name='sal') + @snakes_group.command(name="sal") @locked() async def sal_command(self, ctx: Context) -> None: """ @@ -923,7 +923,7 @@ class Snakes(Cog): await game.open_game() - @snakes_group.command(name='about') + @snakes_group.command(name="about") async def about_command(self, ctx: Context) -> None: """Show an embed with information about the event, its participants, and its winners.""" contributors = [ @@ -968,7 +968,7 @@ class Snakes(Cog): await ctx.send(embed=embed) - @snakes_group.command(name='card') + @snakes_group.command(name="card") async def card_command(self, ctx: Context, *, name: Snake = None) -> None: """ Create an interesting little card from a snake. @@ -978,7 +978,7 @@ class Snakes(Cog): # Get the snake data we need if not name: name_obj = await self._get_snake_name() - name = name_obj['scientific'] + name = name_obj["scientific"] content = await self._get_snek(name) elif isinstance(name, dict): @@ -992,7 +992,7 @@ class Snakes(Cog): stream = BytesIO() async with async_timeout.timeout(10): - async with self.bot.http_session.get(content['image_list'][0]) as response: + async with self.bot.http_session.get(content["image_list"][0]) as response: stream.write(await response.read()) stream.seek(0) @@ -1003,10 +1003,10 @@ class Snakes(Cog): # Send it! await ctx.send( f"A wild {content['name'].title()} appears!", - file=File(final_buffer, filename=content['name'].replace(" ", "") + ".png") + file=File(final_buffer, filename=content["name"].replace(" ", "") + ".png") ) - @snakes_group.command(name='fact') + @snakes_group.command(name="fact") async def fact_command(self, ctx: Context) -> None: """ Gets a snake-related fact. @@ -1022,7 +1022,7 @@ class Snakes(Cog): ) await ctx.send(embed=embed) - @snakes_group.command(name='snakify') + @snakes_group.command(name="snakify") async def snakify_command(self, ctx: Context, *, message: str = None) -> None: """ How would I talk if I were a snake? @@ -1063,7 +1063,7 @@ class Snakes(Cog): await ctx.send(embed=embed) - @snakes_group.command(name='video', aliases=('get_video',)) + @snakes_group.command(name="video", aliases=("get_video",)) async def video_command(self, ctx: Context, *, search: str = None) -> None: """ Gets a YouTube video about snakes. @@ -1074,13 +1074,13 @@ class Snakes(Cog): """ # Are we searching for anything specific? if search: - query = search + ' snake' + query = search + " snake" else: snake = await self._get_snake_name() - query = snake['name'] + query = snake["name"] # Build the URL and make the request - url = 'https://www.googleapis.com/youtube/v3/search' + url = "https://www.googleapis.com/youtube/v3/search" response = await self.bot.http_session.get( url, params={ @@ -1096,14 +1096,14 @@ class Snakes(Cog): # Send the user a video if len(data) > 0: num = random.randint(0, len(data) - 1) - youtube_base_url = 'https://www.youtube.com/watch?v=' + youtube_base_url = "https://www.youtube.com/watch?v=" await ctx.send( content=f"{youtube_base_url}{data[num]['id']['videoId']}" ) else: log.warning(f"YouTube API error. Full response looks like {response}") - @snakes_group.command(name='zen') + @snakes_group.command(name="zen") async def zen_command(self, ctx: Context) -> None: """ Gets a random quote from the Zen of Python, except as if spoken by a snake. diff --git a/bot/exts/evergreen/snakes/_utils.py b/bot/exts/evergreen/snakes/_utils.py index 7d6caf04..d58ee279 100644 --- a/bot/exts/evergreen/snakes/_utils.py +++ b/bot/exts/evergreen/snakes/_utils.py @@ -321,7 +321,7 @@ def create_snek_frame( image_dimensions[Y] / 2 - (dimension_range[Y] / 2 + min_dimensions[Y]) ) - image = Image.new(mode='RGB', size=image_dimensions, color=bg_color) + image = Image.new(mode="RGB", size=image_dimensions, color=bg_color) draw = ImageDraw(image) for index in range(1, len(points)): point = points[index] @@ -345,7 +345,7 @@ def create_snek_frame( def frame_to_png_bytes(image: Image) -> io.BytesIO: """Convert image to byte stream.""" stream = io.BytesIO() - image.save(stream, format='PNG') + image.save(stream, format="PNG") stream.seek(0) return stream @@ -373,7 +373,7 @@ class SnakeAndLaddersGame: self.snakes = snakes self.ctx = context self.channel = self.ctx.channel - self.state = 'booting' + self.state = "booting" self.started = False self.author = self.ctx.author self.players = [] @@ -413,7 +413,7 @@ class SnakeAndLaddersGame: "**Snakes and Ladders**: A new game is about to start!", file=File( str(SNAKE_RESOURCES / "snakes_and_ladders" / "banner.jpg"), - filename='Snakes and Ladders.jpg' + filename="Snakes and Ladders.jpg" ) ) startup = await self.channel.send( @@ -423,7 +423,7 @@ class SnakeAndLaddersGame: for emoji in STARTUP_SCREEN_EMOJI: await startup.add_reaction(emoji) - self.state = 'waiting' + self.state = "waiting" while not self.started: try: @@ -460,7 +460,7 @@ class SnakeAndLaddersGame: self.players.append(user) self.player_tiles[user.id] = 1 - avatar_bytes = await user.avatar_url_as(format='jpeg', size=PLAYER_ICON_IMAGE_SIZE).read() + avatar_bytes = await user.avatar_url_as(format="jpeg", size=PLAYER_ICON_IMAGE_SIZE).read() im = Image.open(io.BytesIO(avatar_bytes)).resize((BOARD_PLAYER_SIZE, BOARD_PLAYER_SIZE)) self.avatar_images[user.id] = im @@ -475,7 +475,7 @@ class SnakeAndLaddersGame: if user == p: await self.channel.send(user.mention + " You are already in the game.", delete_after=10) return - if self.state != 'waiting': + if self.state != "waiting": await self.channel.send(user.mention + " You cannot join at this time.", delete_after=10) return if len(self.players) is MAX_PLAYERS: @@ -510,7 +510,7 @@ class SnakeAndLaddersGame: delete_after=10 ) - if self.state != 'waiting' and len(self.players) == 0: + if self.state != "waiting" and len(self.players) == 0: await self.channel.send("**Snakes and Ladders**: The game has been surrendered!") is_surrendered = True self._destruct() @@ -535,12 +535,12 @@ class SnakeAndLaddersGame: await self.channel.send(user.mention + " Only the author of the game can start it.", delete_after=10) return - if not self.state == 'waiting': + if not self.state == "waiting": await self.channel.send(user.mention + " The game cannot be started at this time.", delete_after=10) return - self.state = 'starting' - player_list = ', '.join(user.mention for user in self.players) + self.state = "starting" + player_list = ", ".join(user.mention for user in self.players) await self.channel.send("**Snakes and Ladders**: The game is starting!\nPlayers: " + player_list) await self.start_round() @@ -556,7 +556,7 @@ class SnakeAndLaddersGame: )) ) - self.state = 'roll' + self.state = "roll" for user in self.players: self.round_has_rolled[user.id] = False board_img = Image.open(str(SNAKE_RESOURCES / "snakes_and_ladders" / "board.jpg")) @@ -574,8 +574,8 @@ class SnakeAndLaddersGame: board_img.paste(self.avatar_images[player.id], box=(x_offset, y_offset)) - board_file = File(frame_to_png_bytes(board_img), filename='Board.jpg') - player_list = '\n'.join((user.mention + ": Tile " + str(self.player_tiles[user.id])) for user in self.players) + board_file = File(frame_to_png_bytes(board_img), filename="Board.jpg") + player_list = "\n".join((user.mention + ": Tile " + str(self.player_tiles[user.id])) for user in self.players) # Store and send new messages temp_board = await self.channel.send( @@ -644,7 +644,7 @@ class SnakeAndLaddersGame: if user.id not in self.player_tiles: await self.channel.send(user.mention + " You are not in the match.", delete_after=10) return - if self.state != 'roll': + if self.state != "roll": await self.channel.send(user.mention + " You may not roll at this time.", delete_after=10) return if self.round_has_rolled[user.id]: @@ -673,7 +673,7 @@ class SnakeAndLaddersGame: async def _complete_round(self) -> None: """At the conclusion of a round check to see if there's been a winner.""" - self.state = 'post_round' + self.state = "post_round" # check for winner winner = self._check_winner() @@ -688,7 +688,7 @@ class SnakeAndLaddersGame: def _check_winner(self) -> Member: """Return a winning member if we're in the post-round state and there's a winner.""" - if self.state != 'post_round': + if self.state != "post_round": return None return next((player for player in self.players if self.player_tiles[player.id] == 100), None) diff --git a/bot/exts/evergreen/source.py b/bot/exts/evergreen/source.py index 14fd02f3..2f25e4cb 100644 --- a/bot/exts/evergreen/source.py +++ b/bot/exts/evergreen/source.py @@ -83,7 +83,7 @@ class BotSource(commands.Cog): url, location, first_line = self.get_source_link(source_object) if isinstance(source_object, commands.Command): - if source_object.cog_name == 'Help': + if source_object.cog_name == "Help": title = "Help Command" description = source_object.__doc__.splitlines()[1] else: diff --git a/bot/exts/evergreen/speedrun.py b/bot/exts/evergreen/speedrun.py index bf6f2117..110d5c13 100644 --- a/bot/exts/evergreen/speedrun.py +++ b/bot/exts/evergreen/speedrun.py @@ -8,7 +8,7 @@ from discord.ext import commands from bot.bot import Bot log = logging.getLogger(__name__) -with Path('bot/resources/evergreen/speedrun_links.json').open(encoding="utf8") as file: +with Path("bot/resources/evergreen/speedrun_links.json").open(encoding="utf8") as file: LINKS = json.load(file) diff --git a/bot/exts/evergreen/status_codes.py b/bot/exts/evergreen/status_codes.py index 635eef3d..a866692e 100644 --- a/bot/exts/evergreen/status_codes.py +++ b/bot/exts/evergreen/status_codes.py @@ -22,10 +22,10 @@ class HTTPStatusCodes(commands.Cog): if not ctx.invoked_subcommand: await invoke_help_command(ctx) - @http_status_group.command(name='cat') + @http_status_group.command(name="cat") async def http_cat(self, ctx: commands.Context, code: int) -> None: """Sends an embed with an image of a cat, portraying the status code.""" - embed = discord.Embed(title=f'**Status: {code}**') + embed = discord.Embed(title=f"**Status: {code}**") url = HTTP_CAT_URL.format(code=code) try: @@ -37,18 +37,18 @@ class HTTPStatusCodes(commands.Cog): raise NotImplementedError except ValueError: - embed.set_footer(text='Inputted status code does not exist.') + embed.set_footer(text="Inputted status code does not exist.") except NotImplementedError: - embed.set_footer(text='Inputted status code is not implemented by http.cat yet.') + embed.set_footer(text="Inputted status code is not implemented by http.cat yet.") finally: await ctx.send(embed=embed) - @http_status_group.command(name='dog') + @http_status_group.command(name="dog") async def http_dog(self, ctx: commands.Context, code: int) -> None: """Sends an embed with an image of a dog, portraying the status code.""" - embed = discord.Embed(title=f'**Status: {code}**') + embed = discord.Embed(title=f"**Status: {code}**") url = HTTP_DOG_URL.format(code=code) try: @@ -60,10 +60,10 @@ class HTTPStatusCodes(commands.Cog): raise NotImplementedError except ValueError: - embed.set_footer(text='Inputted status code does not exist.') + embed.set_footer(text="Inputted status code does not exist.") except NotImplementedError: - embed.set_footer(text='Inputted status code is not implemented by httpstatusdogs.com yet.') + embed.set_footer(text="Inputted status code is not implemented by httpstatusdogs.com yet.") finally: await ctx.send(embed=embed) diff --git a/bot/exts/evergreen/tic_tac_toe.py b/bot/exts/evergreen/tic_tac_toe.py index 1fef427a..7b387c0a 100644 --- a/bot/exts/evergreen/tic_tac_toe.py +++ b/bot/exts/evergreen/tic_tac_toe.py @@ -58,7 +58,7 @@ class Player: ) try: - react, _ = await self.ctx.bot.wait_for('reaction_add', timeout=30.0, check=check_for_move) + react, _ = await self.ctx.bot.wait_for("reaction_add", timeout=30.0, check=check_for_move) except asyncio.TimeoutError: return True, None else: diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py index bfd7d357..1953253b 100644 --- a/bot/exts/evergreen/trivia_quiz.py +++ b/bot/exts/evergreen/trivia_quiz.py @@ -129,7 +129,7 @@ class TriviaQuiz(commands.Cog): ) try: - msg = await self.bot.wait_for('message', check=check, timeout=10) + msg = await self.bot.wait_for("message", check=check, timeout=10) except asyncio.TimeoutError: # In case of TimeoutError and the game has been stopped, then do nothing. if self.game_status[ctx.channel.id] is False: diff --git a/bot/exts/evergreen/wikipedia.py b/bot/exts/evergreen/wikipedia.py index e2172fc3..fa21b916 100644 --- a/bot/exts/evergreen/wikipedia.py +++ b/bot/exts/evergreen/wikipedia.py @@ -20,7 +20,7 @@ WIKI_THUMBNAIL = ( "https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg" "/330px-Wikipedia-logo-v2.svg.png" ) -WIKI_SNIPPET_REGEX = r'(|<[^>]*>)' +WIKI_SNIPPET_REGEX = r"(|<[^>]*>)" WIKI_SEARCH_RESULT = ( "**[{name}]({url})**\n" "{description}\n" @@ -39,18 +39,18 @@ class WikipediaSearch(commands.Cog): async with self.bot.http_session.get(url=url) as resp: if resp.status == 200: raw_data = await resp.json() - number_of_results = raw_data['query']['searchinfo']['totalhits'] + number_of_results = raw_data["query"]["searchinfo"]["totalhits"] if number_of_results: - results = raw_data['query']['search'] + results = raw_data["query"]["search"] lines = [] for article in results: line = WIKI_SEARCH_RESULT.format( - name=article['title'], + name=article["title"], description=unescape( re.sub( - WIKI_SNIPPET_REGEX, '', article['snippet'] + WIKI_SNIPPET_REGEX, "", article["snippet"] ) ), url=f"https://en.wikipedia.org/?curid={article['pageid']}" diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py index c57a8d7a..3cc12c03 100644 --- a/bot/exts/evergreen/wolfram.py +++ b/bot/exts/evergreen/wolfram.py @@ -59,7 +59,7 @@ def custom_cooldown(*ignore: List[int]) -> Callable: A list of roles may be provided to ignore the per-user cooldown. """ async def predicate(ctx: Context) -> bool: - if ctx.invoked_with == 'help': + if ctx.invoked_with == "help": # if the invoked command is help we don't want to increase the ratelimits since it's not actually # invoking the command/making a request, so instead just check if the user/guild are on cooldown. guild_cooldown = not guildcd.get_bucket(ctx.message).get_tokens() == 0 # if guild is on cooldown @@ -118,7 +118,7 @@ async def get_pod_pages(ctx: Context, bot: Bot, query: str) -> Optional[List[Tup request_url = QUERY.format(request="query", data=url_str) async with bot.http_session.get(request_url) as response: - json = await response.json(content_type='text/plain') + json = await response.json(content_type="text/plain") result = json["queryresult"] diff --git a/bot/exts/evergreen/xkcd.py b/bot/exts/evergreen/xkcd.py index ba9e46e0..c98830bc 100644 --- a/bot/exts/evergreen/xkcd.py +++ b/bot/exts/evergreen/xkcd.py @@ -53,7 +53,7 @@ class XKCD(Cog): await ctx.send(embed=embed) return - comic = randint(1, self.latest_comic_info['num']) if comic is None else comic.group(0) + comic = randint(1, self.latest_comic_info["num"]) if comic is None else comic.group(0) if comic == "latest": info = self.latest_comic_info @@ -69,7 +69,7 @@ class XKCD(Cog): return embed.title = f"XKCD comic #{info['num']}" - embed.description = info['alt'] + embed.description = info["alt"] embed.url = f"{BASE_URL}/{info['num']}" if info["img"][-3:] in ("jpg", "png", "gif"): diff --git a/bot/exts/halloween/8ball.py b/bot/exts/halloween/8ball.py index 59d4acc5..d6c5a299 100644 --- a/bot/exts/halloween/8ball.py +++ b/bot/exts/halloween/8ball.py @@ -17,7 +17,7 @@ with Path("bot/resources/halloween/responses.json").open("r", encoding="utf8") a class SpookyEightBall(commands.Cog): """Spooky Eightball answers.""" - @commands.command(aliases=('spooky8ball',)) + @commands.command(aliases=("spooky8ball",)) async def spookyeightball(self, ctx: commands.Context, *, question: str) -> None: """Responds with a random response to a question.""" choice = random.choice(RESPONSES["responses"]) diff --git a/bot/exts/halloween/candy_collection.py b/bot/exts/halloween/candy_collection.py index 5441d8a5..14efa1fb 100644 --- a/bot/exts/halloween/candy_collection.py +++ b/bot/exts/halloween/candy_collection.py @@ -22,11 +22,11 @@ EMOJIS = dict( CANDY="\N{CANDY}", SKULL="\N{SKULL}", MEDALS=( - '\N{FIRST PLACE MEDAL}', - '\N{SECOND PLACE MEDAL}', - '\N{THIRD PLACE MEDAL}', - '\N{SPORTS MEDAL}', - '\N{SPORTS MEDAL}', + "\N{FIRST PLACE MEDAL}", + "\N{SECOND PLACE MEDAL}", + "\N{THIRD PLACE MEDAL}", + "\N{SPORTS MEDAL}", + "\N{SPORTS MEDAL}", ), ) @@ -106,7 +106,7 @@ class CandyCollection(commands.Cog): await self.candy_records.decrement(user.id, lost) if lost == prev_record: - await CandyCollection.send_spook_msg(user, message.channel, 'all of your') + await CandyCollection.send_spook_msg(user, message.channel, "all of your") else: await CandyCollection.send_spook_msg(user, message.channel, lost) else: @@ -125,7 +125,7 @@ class CandyCollection(commands.Cog): """ if random.randint(1, ADD_SKULL_EXISTING_REACTION_CHANCE) == 1: await self.skull_messages.set(message.id, "skull") - await message.add_reaction(EMOJIS['SKULL']) + await message.add_reaction(EMOJIS["SKULL"]) elif random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1: await self.candy_messages.set(message.id, "candy") @@ -173,7 +173,7 @@ class CandyCollection(commands.Cog): ) top_five = top_sorted[:5] - return '\n'.join( + return "\n".join( f"{EMOJIS['MEDALS'][index]} <@{record[0]}>: {record[1]}" for index, record in enumerate(top_five) ) if top_five else "No Candies" @@ -185,7 +185,7 @@ class CandyCollection(commands.Cog): inline=False ) e.add_field( - name='\u200b', + name="\u200b", value="Candies will randomly appear on messages sent. " "\nHit the candy when it appears as fast as possible to get the candy! " "\nBut beware the ghosts...", diff --git a/bot/exts/halloween/hacktober-issue-finder.py b/bot/exts/halloween/hacktober-issue-finder.py index c88e2b6f..baee9612 100644 --- a/bot/exts/halloween/hacktober-issue-finder.py +++ b/bot/exts/halloween/hacktober-issue-finder.py @@ -102,7 +102,7 @@ class HacktoberIssues(commands.Cog): labels = [label["name"] for label in issue["labels"]] embed = discord.Embed(title=title) - embed.description = body[:500] + '...' if len(body) > 500 else body + embed.description = body[:500] + "..." if len(body) > 500 else body embed.add_field(name="labels", value="\n".join(labels)) embed.url = issue_url embed.set_footer(text=issue_url) diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py index 9695ba2a..25da9ad5 100644 --- a/bot/exts/halloween/hacktoberstats.py +++ b/bot/exts/halloween/hacktoberstats.py @@ -138,7 +138,7 @@ class HacktoberStats(commands.Cog): if prs: stats_embed = await self.build_embed(github_username, prs) - await ctx.send('Here are some stats!', embed=stats_embed) + await ctx.send("Here are some stats!", embed=stats_embed) else: await ctx.send(f"No valid Hacktoberfest PRs found for '{github_username}'") @@ -355,7 +355,7 @@ class HacktoberStats(commands.Cog): # loop through reviews and check for approval for item in jsonresp2: - if item.get('status') == "APPROVED": + if item.get("status") == "APPROVED": return True return False @@ -387,9 +387,9 @@ class HacktoberStats(commands.Cog): in_review = [] accepted = [] for pr in prs: - if (pr['created_at'] + timedelta(REVIEW_DAYS)) > now: + if (pr["created_at"] + timedelta(REVIEW_DAYS)) > now: in_review.append(pr) - elif (pr['created_at'] <= oct3) or await self._is_accepted(pr): + elif (pr["created_at"] <= oct3) or await self._is_accepted(pr): accepted.append(pr) return in_review, accepted diff --git a/bot/exts/halloween/halloweenify.py b/bot/exts/halloween/halloweenify.py index 5a8f4ecc..47b20a2a 100644 --- a/bot/exts/halloween/halloweenify.py +++ b/bot/exts/halloween/halloweenify.py @@ -34,8 +34,8 @@ class Halloweenify(commands.Cog): embed.colour = discord.Colour.dark_orange() embed.title = "Not spooky enough?" embed.description = ( - f"**{ctx.author.display_name}** wasn\'t spooky enough for you? That\'s understandable, " - f"{ctx.author.display_name} isn\'t scary at all! " + f"**{ctx.author.display_name}** wasn't spooky enough for you? That's understandable, " + f"{ctx.author.display_name} isn't scary at all! " "Let me think of something better. Hmm... I got it!\n\n " ) embed.set_image(url=image) diff --git a/bot/exts/halloween/monsterbio.py b/bot/exts/halloween/monsterbio.py index f484305d..dbafa43f 100644 --- a/bot/exts/halloween/monsterbio.py +++ b/bot/exts/halloween/monsterbio.py @@ -37,7 +37,7 @@ class MonsterBio(commands.Cog): continue options = seeded_random.sample(TEXT_OPTIONS[key], value) - words[key] = ' '.join(options) + words[key] = " ".join(options) embed = discord.Embed( title=f"{name}'s Biography", diff --git a/bot/exts/halloween/monstersurvey.py b/bot/exts/halloween/monstersurvey.py index 0610503d..486e8937 100644 --- a/bot/exts/halloween/monstersurvey.py +++ b/bot/exts/halloween/monstersurvey.py @@ -9,8 +9,8 @@ from discord.ext.commands import Bot, Cog, Context log = logging.getLogger(__name__) EMOJIS = { - 'SUCCESS': u'\u2705', - 'ERROR': u'\u274C' + "SUCCESS": u"\u2705", + "ERROR": u"\u274C" } @@ -25,63 +25,63 @@ class MonsterSurvey(Cog): def __init__(self): """Initializes values for the bot to use within the voting commands.""" - self.registry_location = os.path.join(os.getcwd(), 'bot', 'resources', 'halloween', 'monstersurvey.json') - with open(self.registry_location, 'r', encoding="utf8") as jason: + self.registry_location = os.path.join(os.getcwd(), "bot", "resources", "halloween", "monstersurvey.json") + with open(self.registry_location, "r", encoding="utf8") as jason: self.voter_registry = json.load(jason) def json_write(self) -> None: """Write voting results to a local JSON file.""" log.info("Saved Monster Survey Results") - with open(self.registry_location, 'w', encoding="utf8") as jason: + with open(self.registry_location, "w", encoding="utf8") as jason: json.dump(self.voter_registry, jason, indent=2) def cast_vote(self, id: int, monster: str) -> None: """ - Cast a user's vote for the specified monster. + Cast a user"s vote for the specified monster. If the user has already voted, their existing vote is removed. """ vr = self.voter_registry for m in vr.keys(): - if id not in vr[m]['votes'] and m == monster: - vr[m]['votes'].append(id) + if id not in vr[m]["votes"] and m == monster: + vr[m]["votes"].append(id) else: - if id in vr[m]['votes'] and m != monster: - vr[m]['votes'].remove(id) + if id in vr[m]["votes"] and m != monster: + vr[m]["votes"].remove(id) def get_name_by_leaderboard_index(self, n: int) -> str: """Return the monster at the specified leaderboard index.""" n = n - 1 vr = self.voter_registry - top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True) + top = sorted(vr, key=lambda k: len(vr[k]["votes"]), reverse=True) name = top[n] if n >= 0 else None return name @commands.group( - name='monster', - aliases=('mon',) + name="monster", + aliases=("mon",) ) async def monster_group(self, ctx: Context) -> None: """The base voting command. If nothing is called, then it will return an embed.""" if ctx.invoked_subcommand is None: async with ctx.typing(): default_embed = Embed( - title='Monster Voting', + title="Monster Voting", color=0xFF6800, - description='Vote for your favorite monster!' + description="Vote for your favorite monster!" ) default_embed.add_field( - name='.monster show monster_name(optional)', - value='Show a specific monster. If none is listed, it will give you an error with valid choices.', + name=".monster show monster_name(optional)", + value="Show a specific monster. If none is listed, it will give you an error with valid choices.", inline=False) default_embed.add_field( - name='.monster vote monster_name', - value='Vote for a specific monster. You get one vote, but can change it at any time.', + name=".monster vote monster_name", + value="Vote for a specific monster. You get one vote, but can change it at any time.", inline=False ) default_embed.add_field( - name='.monster leaderboard', - value='Which monster has the most votes? This command will tell you.', + name=".monster leaderboard", + value="Which monster has the most votes? This command will tell you.", inline=False ) default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry.keys())}") @@ -89,7 +89,7 @@ class MonsterSurvey(Cog): await ctx.send(embed=default_embed) @monster_group.command( - name='vote' + name="vote" ) async def monster_vote(self, ctx: Context, name: str = None) -> None: """ @@ -110,37 +110,37 @@ class MonsterSurvey(Cog): name = name.lower() vote_embed = Embed( - name='Monster Voting', + name="Monster Voting", color=0xFF6800 ) m = self.voter_registry.get(name) if m is None: - vote_embed.description = f'You cannot vote for {name} because it\'s not in the running.' + vote_embed.description = f"You cannot vote for {name} because it's not in the running." vote_embed.add_field( - name='Use `.monster show {monster_name}` for more information on a specific monster', - value='or use `.monster vote {monster}` to cast your vote for said monster.', + name="Use `.monster show {monster_name}` for more information on a specific monster", + value="or use `.monster vote {monster}` to cast your vote for said monster.", inline=False ) vote_embed.add_field( - name='You may vote for or show the following monsters:', - value=f"{', '.join(self.voter_registry.keys())}" + name="You may vote for or show the following monsters:", + value=", ".join(self.voter_registry.keys()) ) else: self.cast_vote(ctx.author.id, name) vote_embed.add_field( - name='Vote successful!', - value=f'You have successfully voted for {m["full_name"]}!', + name="Vote successful!", + value=f"You have successfully voted for {m['full_name']}!", inline=False ) - vote_embed.set_thumbnail(url=m['image']) + vote_embed.set_thumbnail(url=m["image"]) vote_embed.set_footer(text="Please note that any previous votes have been removed.") self.json_write() await ctx.send(embed=vote_embed) @monster_group.command( - name='show' + name="show" ) async def monster_show(self, ctx: Context, name: str = None) -> None: """Shows the named monster. If one is not named, it sends the default voting embed instead.""" @@ -158,31 +158,31 @@ class MonsterSurvey(Cog): m = self.voter_registry.get(name) if not m: - await ctx.send('That monster does not exist.') + await ctx.send("That monster does not exist.") await ctx.invoke(self.monster_vote) return - embed = Embed(title=m['full_name'], color=0xFF6800) - embed.add_field(name='Summary', value=m['summary']) - embed.set_image(url=m['image']) - embed.set_footer(text=f'To vote for this monster, type .monster vote {name}') + embed = Embed(title=m["full_name"], color=0xFF6800) + embed.add_field(name="Summary", value=m["summary"]) + embed.set_image(url=m["image"]) + embed.set_footer(text=f"To vote for this monster, type .monster vote {name}") await ctx.send(embed=embed) @monster_group.command( - name='leaderboard', - aliases=('lb',) + name="leaderboard", + aliases=("lb",) ) async def monster_leaderboard(self, ctx: Context) -> None: """Shows the current standings.""" async with ctx.typing(): vr = self.voter_registry - top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True) - total_votes = sum(len(m['votes']) for m in self.voter_registry.values()) + top = sorted(vr, key=lambda k: len(vr[k]["votes"]), reverse=True) + total_votes = sum(len(m["votes"]) for m in self.voter_registry.values()) embed = Embed(title="Monster Survey Leader Board", color=0xFF6800) for rank, m in enumerate(top): - votes = len(vr[m]['votes']) + votes = len(vr[m]["votes"]) percentage = ((votes / total_votes) * 100) if total_votes > 0 else 0 embed.add_field(name=f"{rank+1}. {vr[m]['full_name']}", value=( diff --git a/bot/exts/halloween/scarymovie.py b/bot/exts/halloween/scarymovie.py index 48c9f53d..f4cf41db 100644 --- a/bot/exts/halloween/scarymovie.py +++ b/bot/exts/halloween/scarymovie.py @@ -47,7 +47,7 @@ class ScaryMovie(commands.Cog): total_pages = data.get("total_pages") # Get movie details from one random result on a random page - params['page'] = random.randint(1, total_pages) + params["page"] = random.randint(1, total_pages) async with self.bot.http_session.get(url=url, params=params, headers=headers) as response: data = await response.json() selection_id = random.choice(data.get("results")).get("id") @@ -71,26 +71,26 @@ class ScaryMovie(commands.Cog): # Get cast names cast = [] - for actor in movie.get('credits', {}).get('cast', [])[:3]: - cast.append(actor.get('name')) + for actor in movie.get("credits", {}).get("cast", [])[:3]: + cast.append(actor.get("name")) # Get director name - director = movie.get('credits', {}).get('crew', []) + director = movie.get("credits", {}).get("crew", []) if director: - director = director[0].get('name') + director = director[0].get("name") # Determine the spookiness rating - rating = '' - rating_count = movie.get('vote_average', 0) / 2 + rating = "" + rating_count = movie.get("vote_average", 0) / 2 for _ in range(int(rating_count)): - rating += ':skull:' + rating += ":skull:" if (rating_count % 1) >= .5: - rating += ':bat:' + rating += ":bat:" # Try to get year of release and runtime - year = movie.get('release_date', [])[:4] - runtime = movie.get('runtime') + year = movie.get("release_date", [])[:4] + runtime = movie.get("runtime") runtime = f"{runtime} minutes" if runtime else None # Not all these attributes will always be present diff --git a/bot/exts/halloween/spookygif.py b/bot/exts/halloween/spookygif.py index bfdf2128..ffb91b1b 100644 --- a/bot/exts/halloween/spookygif.py +++ b/bot/exts/halloween/spookygif.py @@ -19,11 +19,11 @@ class SpookyGif(commands.Cog): async def spookygif(self, ctx: commands.Context) -> None: """Fetches a random gif from the GIPHY API and responds with it.""" async with ctx.typing(): - params = {'api_key': Tokens.giphy, 'tag': 'halloween', 'rating': 'g'} + params = {"api_key": Tokens.giphy, "tag": "halloween", "rating": "g"} # Make a GET request to the Giphy API to get a random halloween gif. - async with self.bot.http_session.get('http://api.giphy.com/v1/gifs/random', params=params) as resp: + async with self.bot.http_session.get("http://api.giphy.com/v1/gifs/random", params=params) as resp: data = await resp.json() - url = data['data']['image_url'] + url = data["data"]["image_url"] embed = discord.Embed(colour=0x9b59b6) embed.title = "A spooooky gif!" diff --git a/bot/exts/halloween/spookynamerate.py b/bot/exts/halloween/spookynamerate.py index 9191f5f6..87172922 100644 --- a/bot/exts/halloween/spookynamerate.py +++ b/bot/exts/halloween/spookynamerate.py @@ -81,7 +81,7 @@ class SpookyNameRate(Cog): # The data cache stores small information such as the current name that is going on and whether it is the first time # the bot is running data = RedisCache() - debug = getenv('SPOOKYNAMERATE_DEBUG', False) # Enable if you do not want to limit the commands to October or if + debug = getenv("SPOOKYNAMERATE_DEBUG", False) # Enable if you do not want to limit the commands to October or if # you do not want to wait till 12 UTC. Note: if debug is enabled and you run `.cogs reload spookynamerate`, it # will automatically start the scoring and announcing the result (without waiting for 12, so do not expect it to.). # Also, it won't wait for the two hours (when the poll closes). diff --git a/bot/exts/halloween/spookyrating.py b/bot/exts/halloween/spookyrating.py index dc398e2e..6c79fbed 100644 --- a/bot/exts/halloween/spookyrating.py +++ b/bot/exts/halloween/spookyrating.py @@ -46,16 +46,16 @@ class SpookyRating(commands.Cog): _, data = SPOOKY_DATA[index] embed = discord.Embed( - title=data['title'], - description=f'{who} scored {spooky_percent}%!', + title=data["title"], + description=f"{who} scored {spooky_percent}%!", color=Colours.orange ) embed.add_field( - name='A whisper from Satan', - value=data['text'] + name="A whisper from Satan", + value=data["text"] ) embed.set_thumbnail( - url=data['image'] + url=data["image"] ) await ctx.send(embed=embed) diff --git a/bot/exts/halloween/spookyreact.py b/bot/exts/halloween/spookyreact.py index dabc3c1f..25e783f4 100644 --- a/bot/exts/halloween/spookyreact.py +++ b/bot/exts/halloween/spookyreact.py @@ -11,13 +11,13 @@ from bot.utils.decorators import in_month log = logging.getLogger(__name__) SPOOKY_TRIGGERS = { - 'spooky': (r"\bspo{2,}ky\b", "\U0001F47B"), - 'skeleton': (r"\bskeleton\b", "\U0001F480"), - 'doot': (r"\bdo{2,}t\b", "\U0001F480"), - 'pumpkin': (r"\bpumpkin\b", "\U0001F383"), - 'halloween': (r"\bhalloween\b", "\U0001F383"), - 'jack-o-lantern': (r"\bjack-o-lantern\b", "\U0001F383"), - 'danger': (r"\bdanger\b", "\U00002620") + "spooky": (r"\bspo{2,}ky\b", "\U0001F47B"), + "skeleton": (r"\bskeleton\b", "\U0001F480"), + "doot": (r"\bdo{2,}t\b", "\U0001F480"), + "pumpkin": (r"\bpumpkin\b", "\U0001F383"), + "halloween": (r"\bhalloween\b", "\U0001F383"), + "jack-o-lantern": (r"\bjack-o-lantern\b", "\U0001F383"), + "danger": (r"\bdanger\b", "\U00002620") } diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index 757a2a1e..4fe3bc09 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -142,14 +142,14 @@ class InternalEval(commands.Cog): log.trace("Sending the formatted output back to the context") await self._send_output(ctx, eval_context.format_output()) - @commands.group(name='internal', aliases=('int',)) + @commands.group(name="internal", aliases=("int",)) @with_role(Roles.admin) async def internal_group(self, ctx: commands.Context) -> None: """Internal commands. Top secret!""" if not ctx.invoked_subcommand: await invoke_help_command(ctx) - @internal_group.command(name='eval', aliases=('e',)) + @internal_group.command(name="eval", aliases=("e",)) @with_role(Roles.admin) async def eval(self, ctx: commands.Context, *, code: str) -> None: """Run eval in a REPL-like format.""" @@ -157,7 +157,7 @@ class InternalEval(commands.Cog): blocks = [block for block in match if block.group("block")] if len(blocks) > 1: - code = '\n'.join(block.group("code") for block in blocks) + code = "\n".join(block.group("code") for block in blocks) else: match = match[0] if len(blocks) == 0 else blocks[0] code, block, lang, delim = match.group("code", "block", "lang", "delim") @@ -168,7 +168,7 @@ class InternalEval(commands.Cog): code = textwrap.dedent(code) await self._eval(ctx, code) - @internal_group.command(name='reset', aliases=("clear", "exit", "r", "c")) + @internal_group.command(name="reset", aliases=("clear", "exit", "r", "c")) @with_role(Roles.admin) async def reset(self, ctx: commands.Context) -> None: """Reset the context and locals of the eval session.""" diff --git a/bot/exts/valentines/be_my_valentine.py b/bot/exts/valentines/be_my_valentine.py index 051f09b8..e94efda0 100644 --- a/bot/exts/valentines/be_my_valentine.py +++ b/bot/exts/valentines/be_my_valentine.py @@ -70,7 +70,7 @@ class BeMyValentine(commands.Cog): await ctx.send("The lovefest role has been successfully removed!") @commands.cooldown(1, 1800, BucketType.user) - @commands.group(name='bemyvalentine', invoke_without_command=True) + @commands.group(name="bemyvalentine", invoke_without_command=True) async def send_valentine( self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None ) -> None: @@ -102,14 +102,14 @@ class BeMyValentine(commands.Cog): valentine, title = self.valentine_check(valentine_type) embed = discord.Embed( - title=f'{emoji_1} {title} {user.display_name} {emoji_2}', - description=f'{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**', + title=f"{emoji_1} {title} {user.display_name} {emoji_2}", + description=f"{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**", color=Colours.pink ) await channel.send(user.mention, embed=embed) @commands.cooldown(1, 1800, BucketType.user) - @send_valentine.command(name='secret') + @send_valentine.command(name="secret") async def anonymous( self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None ) -> None: @@ -137,8 +137,8 @@ class BeMyValentine(commands.Cog): valentine, title = self.valentine_check(valentine_type) embed = discord.Embed( - title=f'{emoji_1}{title} {user.display_name}{emoji_2}', - description=f'{valentine} \n **{emoji_2}From anonymous{emoji_1}**', + title=f"{emoji_1}{title} {user.display_name}{emoji_2}", + description=f"{valentine} \n **{emoji_2}From anonymous{emoji_1}**", color=Colours.pink ) await ctx.message.delete() @@ -154,18 +154,18 @@ class BeMyValentine(commands.Cog): if valentine_type is None: valentine, title = self.random_valentine() - elif valentine_type.lower() in ['p', 'poem']: + elif valentine_type.lower() in ["p", "poem"]: valentine = self.valentine_poem() - title = 'A poem dedicated to' + title = "A poem dedicated to" - elif valentine_type.lower() in ['c', 'compliment']: + elif valentine_type.lower() in ["c", "compliment"]: valentine = self.valentine_compliment() - title = 'A compliment for' + title = "A compliment for" else: # in this case, the user decides to type his own valentine. valentine = valentine_type - title = 'A message for' + title = "A message for" return valentine, title @staticmethod @@ -177,23 +177,23 @@ class BeMyValentine(commands.Cog): def random_valentine(self) -> Tuple[str, str]: """Grabs a random poem or a compliment (any message).""" - valentine_poem = random.choice(self.valentines['valentine_poems']) - valentine_compliment = random.choice(self.valentines['valentine_compliments']) + valentine_poem = random.choice(self.valentines["valentine_poems"]) + valentine_compliment = random.choice(self.valentines["valentine_compliments"]) random_valentine = random.choice([valentine_compliment, valentine_poem]) if random_valentine == valentine_poem: - title = 'A poem dedicated to' + title = "A poem dedicated to" else: - title = 'A compliment for ' + title = "A compliment for " return random_valentine, title def valentine_poem(self) -> str: """Grabs a random poem.""" - valentine_poem = random.choice(self.valentines['valentine_poems']) + valentine_poem = random.choice(self.valentines["valentine_poems"]) return valentine_poem def valentine_compliment(self) -> str: """Grabs a random compliment.""" - valentine_compliment = random.choice(self.valentines['valentine_compliments']) + valentine_compliment = random.choice(self.valentines["valentine_compliments"]) return valentine_compliment diff --git a/bot/exts/valentines/myvalenstate.py b/bot/exts/valentines/myvalenstate.py index 19e6b57f..7a0f8318 100644 --- a/bot/exts/valentines/myvalenstate.py +++ b/bot/exts/valentines/myvalenstate.py @@ -47,12 +47,12 @@ class MyValenstate(commands.Cog): """Find the vacation spot(s) with the most matching characters to the invoking user.""" eq_chars = collections.defaultdict(int) if name is None: - author = ctx.author.name.lower().replace(' ', '') + author = ctx.author.name.lower().replace(" ", "") else: - author = name.lower().replace(' ', '') + author = name.lower().replace(" ", "") for state in STATES.keys(): - lower_state = state.lower().replace(' ', '') + lower_state = state.lower().replace(" ", "") eq_chars[state] = self.levenshtein(author, lower_state) matches = [x for x, y in eq_chars.items() if y == min(eq_chars.values())] @@ -73,8 +73,8 @@ class MyValenstate(commands.Cog): " you better" embed = discord.Embed( - title=f'Your Valenstate is {valenstate} \u2764', - description=f'{STATES[valenstate]["text"]}', + title=f"Your Valenstate is {valenstate} \u2764", + description=f"{STATES[valenstate]['text']}", colour=Colours.pink ) embed.add_field(name=embed_title, value=embed_text) diff --git a/bot/exts/valentines/pickuplines.py b/bot/exts/valentines/pickuplines.py index bb322016..216ee13b 100644 --- a/bot/exts/valentines/pickuplines.py +++ b/bot/exts/valentines/pickuplines.py @@ -25,14 +25,14 @@ class PickupLine(commands.Cog): Note that most of them are very cheesy. """ - random_line = random.choice(pickup_lines['lines']) + random_line = random.choice(pickup_lines["lines"]) embed = discord.Embed( - title=':cheese: Your pickup line :cheese:', - description=random_line['line'], + title=":cheese: Your pickup line :cheese:", + description=random_line["line"], color=Colours.pink ) embed.set_thumbnail( - url=random_line.get('image', pickup_lines['placeholder']) + url=random_line.get("image", pickup_lines["placeholder"]) ) await ctx.send(embed=embed) diff --git a/bot/exts/valentines/savethedate.py b/bot/exts/valentines/savethedate.py index bda5d8c6..ed2d2c5f 100644 --- a/bot/exts/valentines/savethedate.py +++ b/bot/exts/valentines/savethedate.py @@ -23,7 +23,7 @@ class SaveTheDate(commands.Cog): @commands.command() async def savethedate(self, ctx: commands.Context) -> None: """Gives you ideas for what to do on a date with your valentine.""" - random_date = random.choice(VALENTINES_DATES['ideas']) + random_date = random.choice(VALENTINES_DATES["ideas"]) emoji_1 = random.choice(HEART_EMOJIS) emoji_2 = random.choice(HEART_EMOJIS) embed = discord.Embed( diff --git a/bot/exts/valentines/valentine_zodiac.py b/bot/exts/valentines/valentine_zodiac.py index 237fe5db..72fd93fe 100644 --- a/bot/exts/valentines/valentine_zodiac.py +++ b/bot/exts/valentines/valentine_zodiac.py @@ -14,7 +14,7 @@ from bot.constants import Colours log = logging.getLogger(__name__) -LETTER_EMOJI = ':love_letter:' +LETTER_EMOJI = ":love_letter:" HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_heart:", ":two_hearts:"] @@ -32,8 +32,8 @@ class ValentineZodiac(commands.Cog): with explanation_file.open(encoding="utf8") as json_data: zodiac_fact = json.load(json_data) for zodiac_data in zodiac_fact.values(): - zodiac_data['start_at'] = datetime.fromisoformat(zodiac_data['start_at']) - zodiac_data['end_at'] = datetime.fromisoformat(zodiac_data['end_at']) + zodiac_data["start_at"] = datetime.fromisoformat(zodiac_data["start_at"]) + zodiac_data["end_at"] = datetime.fromisoformat(zodiac_data["end_at"]) with compatibility_file.open(encoding="utf8") as json_data: zodiacs = json.load(json_data) @@ -62,10 +62,10 @@ class ValentineZodiac(commands.Cog): log.trace("Making zodiac embed.") embed.title = f"__{zodiac}__" embed.description = self.zodiac_fact[zodiac]["About"] - embed.add_field(name='__Motto__', value=self.zodiac_fact[zodiac]["Motto"], inline=False) - embed.add_field(name='__Strengths__', value=self.zodiac_fact[zodiac]["Strengths"], inline=False) - embed.add_field(name='__Weaknesses__', value=self.zodiac_fact[zodiac]["Weaknesses"], inline=False) - embed.add_field(name='__Full form__', value=self.zodiac_fact[zodiac]["full_form"], inline=False) + embed.add_field(name="__Motto__", value=self.zodiac_fact[zodiac]["Motto"], inline=False) + embed.add_field(name="__Strengths__", value=self.zodiac_fact[zodiac]["Strengths"], inline=False) + embed.add_field(name="__Weaknesses__", value=self.zodiac_fact[zodiac]["Weaknesses"], inline=False) + embed.add_field(name="__Full form__", value=self.zodiac_fact[zodiac]["full_form"], inline=False) embed.set_thumbnail(url=self.zodiac_fact[zodiac]["url"]) else: embed = self.generate_invalidname_embed(zodiac) @@ -79,7 +79,7 @@ class ValentineZodiac(commands.Cog): log.trace("Zodiac name sent.") return zodiac_name - @commands.group(name='zodiac', invoke_without_command=True) + @commands.group(name="zodiac", invoke_without_command=True) async def zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None: """Provides information about zodiac sign by taking zodiac sign name as input.""" final_embed = self.zodiac_build_embed(zodiac_sign) @@ -93,9 +93,9 @@ class ValentineZodiac(commands.Cog): month = month.capitalize() try: month = list(calendar.month_abbr).index(month[:3]) - log.trace('Valid month name entered by user') + log.trace("Valid month name entered by user") except ValueError: - log.info('Invalid month name entered by user') + log.info("Invalid month name entered by user") await ctx.send(f"Sorry, but `{month}` is not a valid month name.") return if (month == 1 and 1 <= date <= 19) or (month == 12 and 22 <= date <= 31): @@ -109,14 +109,14 @@ class ValentineZodiac(commands.Cog): final_embed = discord.Embed() final_embed.color = Colours.soft_red final_embed.description = f"Zodiac sign could not be found because.\n```{e}```" - log.info(f'Error in "zodiac date" command:\n{e}.') + log.info(f"Error in 'zodiac date' command:\n{e}.") else: final_embed = self.zodiac_build_embed(zodiac_sign_based_on_date) await ctx.send(embed=final_embed) log.trace("Embed from date successfully sent.") - @zodiac.command(name="partnerzodiac", aliases=['partner']) + @zodiac.command(name="partnerzodiac", aliases=["partner"]) async def partner_zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None: """Provides a random counter compatible zodiac sign to the given user's zodiac sign.""" embed = discord.Embed() @@ -128,12 +128,12 @@ class ValentineZodiac(commands.Cog): emoji2 = random.choice(HEART_EMOJIS) embed.title = "Zodiac Compatibility" embed.description = ( - f'{zodiac_sign.capitalize()}{emoji1}{compatible_zodiac["Zodiac"]}\n' - f'{emoji2}Compatibility meter : {compatible_zodiac["compatibility_score"]}{emoji2}' + f"{zodiac_sign.capitalize()}{emoji1}{compatible_zodiac['Zodiac']}\n" + f"{emoji2}Compatibility meter : {compatible_zodiac['compatibility_score']}{emoji2}" ) embed.add_field( - name=f'A letter from Dr.Zodiac {LETTER_EMOJI}', - value=compatible_zodiac['description'] + name=f"A letter from Dr.Zodiac {LETTER_EMOJI}", + value=compatible_zodiac["description"] ) else: embed = self.generate_invalidname_embed(zodiac_sign) diff --git a/bot/exts/valentines/whoisvalentine.py b/bot/exts/valentines/whoisvalentine.py index 73cdcf52..3789fad5 100644 --- a/bot/exts/valentines/whoisvalentine.py +++ b/bot/exts/valentines/whoisvalentine.py @@ -18,17 +18,17 @@ with open(Path("bot/resources/valentines/valentine_facts.json"), "r", encoding=" class ValentineFacts(commands.Cog): """A Cog for displaying facts about Saint Valentine.""" - @commands.command(aliases=('whoisvalentine', 'saint_valentine')) + @commands.command(aliases=("whoisvalentine", "saint_valentine")) async def who_is_valentine(self, ctx: commands.Context) -> None: """Displays info about Saint Valentine.""" embed = discord.Embed( title="Who is Saint Valentine?", - description=FACTS['whois'], + description=FACTS["whois"], color=Colours.pink ) embed.set_thumbnail( - url='https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Saint_Valentine_-_' - 'facial_reconstruction.jpg/1024px-Saint_Valentine_-_facial_reconstruction.jpg' + url="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Saint_Valentine_-_" + "facial_reconstruction.jpg/1024px-Saint_Valentine_-_facial_reconstruction.jpg" ) await ctx.send(embed=embed) @@ -37,8 +37,8 @@ class ValentineFacts(commands.Cog): async def valentine_fact(self, ctx: commands.Context) -> None: """Shows a random fact about Valentine's Day.""" embed = discord.Embed( - title=choice(FACTS['titles']), - description=choice(FACTS['text']), + title=choice(FACTS["titles"]), + description=choice(FACTS["text"]), color=Colours.pink ) diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 35ef0a7b..2fac2086 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -43,12 +43,12 @@ async def disambiguate( or if the user makes an invalid choice. """ if len(entries) == 0: - raise BadArgument('No matches found.') + raise BadArgument("No matches found.") if len(entries) == 1: return entries[0] - choices = (f'{index}: {entry}' for index, entry in enumerate(entries, start=1)) + choices = (f"{index}: {entry}" for index, entry in enumerate(entries, start=1)) def check(message: discord.Message) -> bool: return (message.content.isdigit() @@ -59,7 +59,7 @@ async def disambiguate( if embed is None: embed = discord.Embed() - coro1 = ctx.bot.wait_for('message', check=check, timeout=timeout) + coro1 = ctx.bot.wait_for("message", check=check, timeout=timeout) coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=entries_per_page, empty=empty, max_size=6000, timeout=9000) @@ -74,7 +74,7 @@ async def disambiguate( if result is None: for coro in pending: coro.cancel() - raise BadArgument('Canceled.') + raise BadArgument("Canceled.") # Pagination was not initiated, only one page if result.author == ctx.bot.user: @@ -85,7 +85,7 @@ async def disambiguate( for coro in pending: coro.cancel() except asyncio.TimeoutError: - raise BadArgument('Timed out.') + raise BadArgument("Timed out.") # Guaranteed to not error because of isdigit() in check index = int(result.content) @@ -93,7 +93,7 @@ async def disambiguate( try: return entries[index - 1] except IndexError: - raise BadArgument('Invalid choice.') + raise BadArgument("Invalid choice.") def replace_many( @@ -139,7 +139,7 @@ def replace_many( return replacement # Clean punctuation from word so string methods work - cleaned_word = word.translate(str.maketrans('', '', string.punctuation)) + cleaned_word = word.translate(str.maketrans("", "", string.punctuation)) if cleaned_word.isupper(): return replacement.upper() elif cleaned_word[0].isupper(): diff --git a/bot/utils/checks.py b/bot/utils/checks.py index 9dd4dde0..3783dd38 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -154,8 +154,8 @@ def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketTy # # If the `before_invoke` detail is ever a problem then I can quickly just swap over. if not isinstance(command, Command): - raise TypeError('Decorator `cooldown_with_role_bypass` must be applied after the command decorator. ' - 'This means it has to be above the command decorator in the code.') + raise TypeError("Decorator `cooldown_with_role_bypass` must be applied after the command decorator. " + "This means it has to be above the command decorator in the code.") command._before_invoke = predicate diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py index 60066dc4..c0783144 100644 --- a/bot/utils/decorators.py +++ b/bot/utils/decorators.py @@ -269,7 +269,7 @@ def whitelist_check(**default_kwargs: t.Container[int]) -> t.Callable[[Context], channels.update(channel.id for channel in category.text_channels) if channels: - channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) + channels_str = ", ".join(f"<#{c_id}>" for c_id in channels) message = f"Sorry, but you may only use this command within {channels_str}." else: message = "Sorry, but you may not use this command." diff --git a/bot/utils/extensions.py b/bot/utils/extensions.py index 459588a1..cd491c4b 100644 --- a/bot/utils/extensions.py +++ b/bot/utils/extensions.py @@ -35,8 +35,8 @@ def walk_extensions() -> Iterator[str]: async def invoke_help_command(ctx: Context) -> None: """Invoke the help command or default help command if help extensions is not loaded.""" - if 'bot.exts.evergreen.help' in ctx.bot.extensions: - help_command = ctx.bot.get_command('help') + if "bot.exts.evergreen.help" in ctx.bot.extensions: + help_command = ctx.bot.get_command("help") await ctx.invoke(help_command, ctx.command.qualified_name) return await ctx.send_help(ctx.command) diff --git a/bot/utils/halloween/spookifications.py b/bot/utils/halloween/spookifications.py index 11f69850..f69dd6fd 100644 --- a/bot/utils/halloween/spookifications.py +++ b/bot/utils/halloween/spookifications.py @@ -13,16 +13,16 @@ def inversion(im: Image) -> Image: Returns an inverted image when supplied with an Image object. """ - im = im.convert('RGB') + im = im.convert("RGB") inv = ImageOps.invert(im) return inv def pentagram(im: Image) -> Image: """Adds pentagram to the image.""" - im = im.convert('RGB') + im = im.convert("RGB") wt, ht = im.size - penta = Image.open('bot/resources/halloween/bloody-pentagram.png') + penta = Image.open("bot/resources/halloween/bloody-pentagram.png") penta = penta.resize((wt, ht)) im.paste(penta, (0, 0), penta) return im @@ -35,9 +35,9 @@ def bat(im: Image) -> Image: The bat silhoutte is of a size at least one-fifths that of the original image and may be rotated up to 90 degrees anti-clockwise. """ - im = im.convert('RGB') + im = im.convert("RGB") wt, ht = im.size - bat = Image.open('bot/resources/halloween/bat-clipart.png') + bat = Image.open("bot/resources/halloween/bat-clipart.png") bat_size = randint(wt//10, wt//7) rot = randint(0, 90) bat = bat.resize((bat_size, bat_size)) diff --git a/bot/utils/pagination.py b/bot/utils/pagination.py index a97dd023..a073a00b 100644 --- a/bot/utils/pagination.py +++ b/bot/utils/pagination.py @@ -26,7 +26,7 @@ class EmptyPaginatorEmbed(Exception): class LinePaginator(Paginator): """A class that aids in paginating code blocks for Discord messages.""" - def __init__(self, prefix: str = '```', suffix: str = '```', max_size: int = 2000, max_lines: int = None): + def __init__(self, prefix: str = "```", suffix: str = "```", max_size: int = 2000, max_lines: int = None): """ Overrides the Paginator.__init__ from inside discord.ext.commands. @@ -44,7 +44,7 @@ class LinePaginator(Paginator): self._count = len(prefix) + 1 # prefix + newline self._pages = [] - def add_line(self, line: str = '', *, empty: bool = False) -> None: + def add_line(self, line: str = "", *, empty: bool = False) -> None: """ Adds a line to the current page. @@ -56,7 +56,7 @@ class LinePaginator(Paginator): If `empty` is True, an empty line will be placed after the a given `line`. """ if len(line) > self.max_size - len(self.prefix) - 2: - raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2)) + raise RuntimeError("Line exceeds maximum page size %s" % (self.max_size - len(self.prefix) - 2)) if self.max_lines is not None: if self._linecount >= self.max_lines: @@ -71,7 +71,7 @@ class LinePaginator(Paginator): self._current_page.append(line) if empty: - self._current_page.append('') + self._current_page.append("") self._count += 1 @classmethod -- cgit v1.2.3 From 52aa6dcd499fc3e757a226c1192755888a6d88ef Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Mon, 10 May 2021 09:30:46 -0400 Subject: chore: ctx.message.author -> ctx.author --- bot/exts/easter/bunny_name_generator.py | 2 +- bot/exts/evergreen/snakes/_snakes_cog.py | 6 +++--- bot/exts/halloween/monsterbio.py | 2 +- bot/exts/internal_eval/_internal_eval.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/easter/bunny_name_generator.py b/bot/exts/easter/bunny_name_generator.py index b6523ff6..5e3b014d 100644 --- a/bot/exts/easter/bunny_name_generator.py +++ b/bot/exts/easter/bunny_name_generator.py @@ -62,7 +62,7 @@ class BunnyNameGenerator(commands.Cog): @commands.command() async def bunnifyme(self, ctx: commands.Context) -> None: """Gets your Discord username and bunnifies it.""" - username = ctx.message.author.display_name + username = ctx.author.display_name # If name contains spaces or other separators, get the individual words to randomly bunnify spaces_in_name = self.find_separators(username) diff --git a/bot/exts/evergreen/snakes/_snakes_cog.py b/bot/exts/evergreen/snakes/_snakes_cog.py index 6278c883..b844960a 100644 --- a/bot/exts/evergreen/snakes/_snakes_cog.py +++ b/bot/exts/evergreen/snakes/_snakes_cog.py @@ -746,7 +746,7 @@ class Snakes(Cog): my_snake_embed = Embed(description=":tada: Congrats! You hatched: **{0}**".format(snake_name)) my_snake_embed.set_thumbnail(url=snake_image) my_snake_embed.set_footer( - text=" Owner: {0}#{1}".format(ctx.message.author.name, ctx.message.author.discriminator) + text=" Owner: {0}#{1}".format(ctx.author.name, ctx.author.discriminator) ) await ctx.send(embed=my_snake_embed) @@ -1046,14 +1046,14 @@ class Snakes(Cog): """ with ctx.typing(): embed = Embed() - user = ctx.message.author + user = ctx.author if not message: # Get a random message from the users history messages = [] async for message in ctx.channel.history(limit=500).filter( - lambda msg: msg.author == ctx.message.author # Message was sent by author. + lambda msg: msg.author == ctx.author # Message was sent by author. ): messages.append(message.content) diff --git a/bot/exts/halloween/monsterbio.py b/bot/exts/halloween/monsterbio.py index dbafa43f..1aaba7bb 100644 --- a/bot/exts/halloween/monsterbio.py +++ b/bot/exts/halloween/monsterbio.py @@ -26,7 +26,7 @@ class MonsterBio(commands.Cog): @commands.command(brief="Sends your monster bio!") async def monsterbio(self, ctx: commands.Context) -> None: """Sends a description of a monster.""" - seeded_random = random.Random(ctx.message.author.id) # Seed a local Random instance rather than the system one + seeded_random = random.Random(ctx.author.id) # Seed a local Random instance rather than the system one name = self.generate_name(seeded_random) species = self.generate_name(seeded_random) diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index 4fe3bc09..56bf5add 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -114,7 +114,7 @@ class InternalEval(commands.Cog): """Evaluate the `code` in the current evaluation context.""" context_vars = { "message": ctx.message, - "author": ctx.message.author, + "author": ctx.author, "channel": ctx.channel, "guild": ctx.guild, "ctx": ctx, -- cgit v1.2.3 From 48d43ea492a5f4bbbf67dd32dc6e5bff4f5053d2 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Thu, 26 Aug 2021 13:57:30 +0100 Subject: Limit internal eval commands to owner if bot in debug mode --- bot/exts/internal_eval/_internal_eval.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'bot/exts/internal_eval') diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py index 56bf5add..b7749144 100644 --- a/bot/exts/internal_eval/_internal_eval.py +++ b/bot/exts/internal_eval/_internal_eval.py @@ -7,7 +7,7 @@ import discord from discord.ext import commands from bot.bot import Bot -from bot.constants import Roles +from bot.constants import Client, Roles from bot.utils.decorators import with_role from bot.utils.extensions import invoke_help_command from ._helpers import EvalContext @@ -41,6 +41,9 @@ class InternalEval(commands.Cog): self.bot = bot self.locals = {} + if Client.debug: + self.internal_group.add_check(commands.is_owner().predicate) + @staticmethod def shorten_output( output: str, -- cgit v1.2.3