From 08a905b88af14849a9602b7618c6b073c5a17ce8 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 4 Aug 2021 17:20:57 -0400 Subject: [PATCH 001/169] feat: extension loading system --- modmail/__init__.py | 6 + modmail/__main__.py | 4 +- modmail/bot.py | 27 +++- modmail/exts/__init__.py | 0 modmail/exts/utils/__init__.py | 0 modmail/exts/utils/extensions.py | 270 +++++++++++++++++++++++++++++++ modmail/utils/extensions.py | 37 +++++ pyproject.toml | 2 +- 8 files changed, 339 insertions(+), 7 deletions(-) create mode 100644 modmail/exts/__init__.py create mode 100644 modmail/exts/utils/__init__.py create mode 100644 modmail/exts/utils/extensions.py create mode 100644 modmail/utils/extensions.py diff --git a/modmail/__init__.py b/modmail/__init__.py index 6cf37526..81ab0470 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -1,11 +1,15 @@ import logging import logging.handlers from pathlib import Path +from typing import TYPE_CHECKING import coloredlogs from .log import ModmailLogger +if TYPE_CHECKING: + from modmail.bot import ModmailBot + logging.TRACE = 5 logging.NOTICE = 25 logging.addLevelName(logging.TRACE, "TRACE") @@ -55,3 +59,5 @@ logging.getLogger("asyncio").setLevel(logging.INFO) root.debug("Logging initialization complete") + +instance: "ModmailBot" = None # Global ModmailBot instance. diff --git a/modmail/__main__.py b/modmail/__main__.py index ae68d2dd..54b4d33c 100644 --- a/modmail/__main__.py +++ b/modmail/__main__.py @@ -17,7 +17,9 @@ def main() -> None: """Run the bot.""" bot = ModmailBot() log.notice("running bot") - bot.run(bot.config.bot.token) + bot.instance = ModmailBot() + bot.instance.load_extensions() + bot.instance.run(bot.config.bot.token) if __name__ == "__main__": diff --git a/modmail/bot.py b/modmail/bot.py index 3284b89f..3e288e2f 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -6,7 +6,7 @@ from aiohttp import ClientSession from discord.ext import commands -from .config import CONFIG, INTERNAL +from modmail.config import CONFIG, INTERNAL log = logging.getLogger(__name__) @@ -36,8 +36,6 @@ async def get_prefix(self, message: discord.Message = None) -> t.List[str]: async def close(self) -> None: """Safely close HTTP session and extensions when bot is shutting down.""" - await super().close() - for ext in list(self.extensions): try: self.unload_extension(ext) @@ -50,11 +48,21 @@ async def close(self) -> None: except Exception: log.error(f"Exception occured while removing cog {cog.name}", exc_info=1) - await super().close() - if self.http_session: await self.http_session.close() + await super().close() + + def load_extensions(self) -> None: + """Load all enabled extensions.""" + # Must be done here to avoid a circular import. + from modmail.utils.extensions import EXTENSIONS + + extensions = set(EXTENSIONS) # Create a mutable copy. + + for extension in extensions: + self.load_extension(extension) + def add_cog(self, cog: commands.Cog) -> None: """ Delegate to super to register `cog`. @@ -64,6 +72,15 @@ def add_cog(self, cog: commands.Cog) -> None: super().add_cog(cog) log.info(f"Cog loaded: {cog.qualified_name}") + def remove_cog(self, cog: commands.Cog) -> None: + """ + Delegate to super to unregister `cog`. + + This only serves to make the debug log, so that extensions don't have to. + """ + super().remove_cog(cog) + log.trace(f"Cog unloaded: {cog}") + async def on_ready(self) -> None: """Send basic login success message.""" log.info("Logged in as %s", self.user) diff --git a/modmail/exts/__init__.py b/modmail/exts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modmail/exts/utils/__init__.py b/modmail/exts/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/utils/extensions.py new file mode 100644 index 00000000..ab75eba2 --- /dev/null +++ b/modmail/exts/utils/extensions.py @@ -0,0 +1,270 @@ +# original source: +# https://github.com/python-discord/bot/blob/a8869b4d60512b173871c886321b261cbc4acca9/bot/exts/utils/extensions.py # noqa: E501 +# MIT License 2021 Python Discord +import functools +import logging +import typing as t +from enum import Enum + +from discord import Colour, Embed +from discord.ext import commands +from discord.ext.commands import Context, group + +from modmail import exts +from modmail.bot import ModmailBot +from modmail.log import ModmailLogger +from modmail.utils.extensions import EXTENSIONS, unqualify + +log: ModmailLogger = logging.getLogger(__name__) + + +UNLOAD_BLACKLIST = { + __name__, +} +BASE_PATH_LEN = len(exts.__name__.split(".")) + +log.notice(UNLOAD_BLACKLIST) + + +class Action(Enum): + """Represents an action to perform on an extension.""" + + # Need to be partial otherwise they are considered to be function definitions. + LOAD = functools.partial(ModmailBot.load_extension) + UNLOAD = functools.partial(ModmailBot.unload_extension) + RELOAD = functools.partial(ModmailBot.reload_extension) + + +class Extension(commands.Converter): + """ + Fully qualify the name of an extension and ensure it exists. + + The * and ** values bypass this when used with the reload command. + """ + + async def convert(self, ctx: Context, argument: str) -> str: + """Fully qualify the name of an extension and ensure it exists.""" + # Special values to reload all extensions + if argument == "*" or argument == "**": + return argument + + argument = argument.lower() + + if argument in EXTENSIONS: + return argument + elif (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS: + return qualified_arg + + matches = [] + for ext in EXTENSIONS: + if argument == unqualify(ext): + matches.append(ext) + + if len(matches) > 1: + matches.sort() + names = "\n".join(matches) + raise commands.BadArgument( + f":x: `{argument}` is an ambiguous extension name. " + f"Please use one of the following fully-qualified names.```\n{names}```" + ) + elif matches: + return matches[0] + else: + raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") + + +class Extensions(commands.Cog): + """Extension management commands.""" + + def __init__(self, bot: ModmailBot): + self.bot = bot + + @group(name="extensions", aliases=("ext", "exts", "c", "cogs"), invoke_without_command=True) + async def extensions_group(self, ctx: Context) -> None: + """Load, unload, reload, and list loaded extensions.""" + await ctx.send_help(ctx.command) + + @extensions_group.command(name="load", aliases=("l",)) + async def load_command(self, ctx: Context, *extensions: Extension) -> None: + r""" + Load extensions given their fully qualified or unqualified names. + + If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. + """ # noqa: W605 + if not extensions: + await ctx.send_help(ctx.command) + return + + if "*" in extensions or "**" in extensions: + extensions = set(EXTENSIONS) - set(self.bot.extensions.keys()) + + msg = self.batch_manage(Action.LOAD, *extensions) + await ctx.send(msg) + + @extensions_group.command(name="unload", aliases=("ul",)) + async def unload_command(self, ctx: Context, *extensions: Extension) -> None: + r""" + Unload currently loaded extensions given their fully qualified or unqualified names. + + If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. + """ # noqa: W605 + if not extensions: + await ctx.send_help(ctx.command) + return + + blacklisted = "\n".join(UNLOAD_BLACKLIST & set(extensions)) + + if blacklisted: + msg = f":x: The following extension(s) may not be unloaded:```\n{blacklisted}```" + else: + if "*" in extensions or "**" in extensions: + extensions = set(self.bot.extensions.keys()) - UNLOAD_BLACKLIST + + msg = self.batch_manage(Action.UNLOAD, *extensions) + + await ctx.send(msg) + + @extensions_group.command(name="reload", aliases=("r",)) + async def reload_command(self, ctx: Context, *extensions: Extension) -> None: + r""" + Reload extensions given their fully qualified or unqualified names. + + If an extension fails to be reloaded, it will be rolled-back to the prior working state. + + If '\*' is given as the name, all currently loaded extensions will be reloaded. + If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. + """ # noqa: W605 + if not extensions: + await ctx.send_help(ctx.command) + return + + if "**" in extensions: + extensions = EXTENSIONS + elif "*" in extensions: + extensions = set(self.bot.extensions.keys()) | set(extensions) + extensions.remove("*") + + msg = self.batch_manage(Action.RELOAD, *extensions) + + await ctx.send(msg) + + @extensions_group.command(name="list", aliases=("all",)) + async def list_command(self, ctx: Context) -> None: + """ + Get a list of all extensions, including their loaded status. + + Grey indicates that the extension is unloaded. + Green indicates that the extension is currently loaded. + """ + embed = Embed(colour=Colour.blurple()) + embed.set_author( + name="Extensions List", + ) + + lines = [] + categories = self.group_extension_statuses() + for category, extensions in sorted(categories.items()): + # Treat each category as a single line by concatenating everything. + # This ensures the paginator will not cut off a page in the middle of a category. + category = category.replace("_", " ").title() + extensions = "\n".join(sorted(extensions)) + lines.append(f"**{category}**\n{extensions}\n") + + log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") + # await Paginator.paginate(lines, ctx, embed, scale_to_size=700, empty=False) + await ctx.send(lines) + + def group_extension_statuses(self) -> t.Mapping[str, str]: + """Return a mapping of extension names and statuses to their categories.""" + categories = {} + + for ext in EXTENSIONS: + if ext in self.bot.extensions: + status = ":thumbsup:" + else: + status = ":thumbsdown:" + + path = ext.split(".") + if len(path) > BASE_PATH_LEN + 1: + category = " - ".join(path[BASE_PATH_LEN:-1]) + else: + category = "uncategorised" + + categories.setdefault(category, []).append(f"{status} {path[-1]}") + + return categories + + def batch_manage(self, action: Action, *extensions: str) -> str: + """ + Apply an action to multiple extensions and return a message with the results. + + If only one extension is given, it is deferred to `manage()`. + """ + if len(extensions) == 1: + msg, _ = self.manage(action, extensions[0]) + return msg + + verb = action.name.lower() + failures = {} + + for extension in extensions: + _, error = self.manage(action, extension) + if error: + failures[extension] = error + + emoji = ":x:" if failures else ":ok_hand:" + msg = f"{emoji} {len(extensions) - len(failures)} / {len(extensions)} extensions {verb}ed." + + if failures: + failures = "\n".join(f"{ext}\n {err}" for ext, err in failures.items()) + msg += f"\nFailures:```\n{failures}```" + + log.debug(f"Batch {verb}ed extensions.") + + return msg + + def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: + """Apply an action to an extension and return the status message and any error message.""" + verb = action.name.lower() + error_msg = None + + try: + action.value(self.bot, ext) + except (commands.ExtensionAlreadyLoaded, commands.ExtensionNotLoaded): + if action is Action.RELOAD: + # When reloading, just load the extension if it was not loaded. + log.debug("Treating {ext!r} as if it was not loaded.") + return self.manage(Action.LOAD, ext) + + msg = f":x: Extension `{ext}` is already {verb}ed." + log.debug(msg[4:]) + except Exception as e: + if hasattr(e, "original"): + e = e.original + + log.exception(f"Extension '{ext}' failed to {verb}.") + + error_msg = f"{e.__class__.__name__}: {e}" + msg = f":x: Failed to {verb} extension `{ext}`:\n```\n{error_msg}```" + else: + msg = f":ok_hand: Extension successfully {verb}ed: `{ext}`." + log.debug(msg[10:]) + + return msg, error_msg + + # This cannot be static (must have a __func__ attribute). + async def cog_check(self, ctx: Context) -> bool: + """Only allow bot owners to invoke the commands in this cog.""" + return await self.bot.is_owner(ctx.author) + + # This cannot be static (must have a __func__ attribute). + async def cog_command_error(self, ctx: Context, error: Exception) -> None: + """Handle BadArgument errors locally to prevent the help command from showing.""" + if isinstance(error, commands.BadArgument): + await ctx.send(str(error)) + error.handled = True + + +def setup(bot: ModmailBot) -> None: + """Load the Extensions cog.""" + bot.add_cog(Extensions(bot)) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py new file mode 100644 index 00000000..24f4593b --- /dev/null +++ b/modmail/utils/extensions.py @@ -0,0 +1,37 @@ +# original source: +# https://github.com/python-discord/bot/blob/a8869b4d60512b173871c886321b261cbc4acca9/bot/utils/extensions.py +# MIT License 2021 Python Discord +import importlib +import inspect +import pkgutil +from typing import Iterator, NoReturn + +from modmail import exts + + +def unqualify(name: str) -> str: + """Return an unqualified name given a qualified module/package `name`.""" + return name.rsplit(".", maxsplit=1)[-1] + + +def walk_extensions() -> Iterator[str]: + """Yield extension names from the modmail.exts subpackage.""" + + def on_error(name: str) -> NoReturn: + raise ImportError(name=name) # pragma: no cover + + for module in pkgutil.walk_packages(exts.__path__, f"{exts.__name__}.", onerror=on_error): + if unqualify(module.name).startswith("_"): + # Ignore module/package names starting with an underscore. + continue + + if module.ispkg: + imported = importlib.import_module(module.name) + if not inspect.isfunction(getattr(imported, "setup", None)): + # If it lacks a setup function, it's not an extension. + continue + + yield module.name + + +EXTENSIONS = frozenset(walk_extensions()) diff --git a/pyproject.toml b/pyproject.toml index 89da5099..94ccef10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "Modmail" -version = "0.0.1" +version = "0.1.0" description = "A modmail bot for Discord. Python 3.8+ compatiable" license = "MIT" From e18656eed9ac04fd543e24b4ee4cec14a68f2be1 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 4 Aug 2021 17:36:07 -0400 Subject: [PATCH 002/169] fix: profile process type and runtime.txt --- Procfile | 2 +- runtime.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 runtime.txt diff --git a/Procfile b/Procfile index 8faf34e1..135be655 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -worker: python -m modmail +web: python -m modmail diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 00000000..9bff0e00 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.9.6 From feec5f6ca3d09a3e81b46c38ee6c2157d1a8597b Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 4 Aug 2021 17:39:57 -0400 Subject: [PATCH 003/169] chore: temporarily lower logging level --- modmail/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index 81ab0470..3a9076e2 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -15,7 +15,7 @@ logging.addLevelName(logging.TRACE, "TRACE") logging.addLevelName(logging.NOTICE, "NOTICE") -LOG_LEVEL = 20 +LOG_LEVEL = 5 fmt = "%(asctime)s %(levelname)10s %(name)15s - [%(lineno)5d]: %(message)s" datefmt = "%Y/%m/%d %H:%M:%S" @@ -43,13 +43,14 @@ file_handler.setLevel(logging.TRACE) coloredlogs.install( - level=LOG_LEVEL, + level=logging.TRACE, fmt=fmt, datefmt=datefmt, ) # Create root logger root: ModmailLogger = logging.getLogger() +root.setLevel(LOG_LEVEL) root.addHandler(file_handler) # Silence irrelevant loggers From d05a49bbeee2ad6516d7052fb9f5f4c4b37a6597 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 4 Aug 2021 18:36:08 -0400 Subject: [PATCH 004/169] chore: remove erroneous logging statement --- modmail/exts/utils/extensions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/utils/extensions.py index ab75eba2..a0fdf4de 100644 --- a/modmail/exts/utils/extensions.py +++ b/modmail/exts/utils/extensions.py @@ -23,8 +23,6 @@ } BASE_PATH_LEN = len(exts.__name__.split(".")) -log.notice(UNLOAD_BLACKLIST) - class Action(Enum): """Represents an action to perform on an extension.""" From af5528e6cc1a2fdce12216c32a8b081e2b2438ae Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 4 Aug 2021 18:47:46 -0400 Subject: [PATCH 005/169] feat: add uptime and ping commands --- modmail/bot.py | 2 ++ modmail/exts/meta.py | 32 ++++++++++++++++++++++++++++++++ poetry.lock | 34 ++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + requirements.txt | 3 +++ 5 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 modmail/exts/meta.py diff --git a/modmail/bot.py b/modmail/bot.py index 3e288e2f..1775b06f 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -2,6 +2,7 @@ import logging import typing as t +import arrow import discord from aiohttp import ClientSession from discord.ext import commands @@ -24,6 +25,7 @@ def __init__(self, **kwargs): self.config = CONFIG self.internal = INTERNAL self.http_session: ClientSession = None + self.start_time = arrow.utcnow() super().__init__(command_prefix=self.get_prefix, **kwargs) async def create_session(self) -> None: diff --git a/modmail/exts/meta.py b/modmail/exts/meta.py new file mode 100644 index 00000000..199ced69 --- /dev/null +++ b/modmail/exts/meta.py @@ -0,0 +1,32 @@ +import logging + +from discord.ext import commands +from discord.ext.commands import Context + +from modmail.bot import ModmailBot +from modmail.log import ModmailLogger + +log: ModmailLogger = logging.getLogger(__name__) + + +class Meta(commands.Cog): + """The description for Ping goes here.""" + + def __init__(self, bot: ModmailBot): + self.bot = bot + + @commands.command() + async def ping(self, ctx: Context) -> None: + """Check response time.""" + await ctx.send(f"{round(self.bot.latency * 1000)}ms") + + @commands.command(name="uptime") + async def uptime(self, ctx: commands.Context) -> None: + """Get the current uptime of the bot.""" + timestamp = self.bot.start_time.format("X").split(".")[0] + await ctx.send(f"Start time: ") + + +def setup(bot: ModmailBot) -> None: + """Load the Meta cog.""" + bot.add_cog(Meta(bot)) diff --git a/poetry.lock b/poetry.lock index 1c609571..3a54b0e2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -39,6 +39,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "arrow" +version = "1.1.1" +description = "Better dates & times for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +python-dateutil = ">=2.7.0" + [[package]] name = "async-timeout" version = "3.0.1" @@ -815,6 +826,17 @@ pytest-forked = "*" psutil = ["psutil (>=3.0)"] testing = ["filelock"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "0.17.1" @@ -864,7 +886,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" @@ -993,7 +1015,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "c7a0c52a052d6ecf36dcae9afdfdeee0c3bb98c15af1d72185254766d7f33028" +content-hash = "2172524caa421ed11d12e95572a62ed432b8a295022440e94f4f480c917e7bc7" [metadata.files] aiodns = [ @@ -1043,6 +1065,10 @@ appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] +arrow = [ + {file = "arrow-1.1.1-py3-none-any.whl", hash = "sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510"}, + {file = "arrow-1.1.1.tar.gz", hash = "sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a"}, +] async-timeout = [ {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, @@ -1578,6 +1604,10 @@ pytest-xdist = [ {file = "pytest-xdist-2.3.0.tar.gz", hash = "sha256:e8ecde2f85d88fbcadb7d28cb33da0fa29bca5cf7d5967fa89fc0e97e5299ea5"}, {file = "pytest_xdist-2.3.0-py3-none-any.whl", hash = "sha256:ed3d7da961070fce2a01818b51f6888327fb88df4379edeb6b9d990e789d9c8d"}, ] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] python-dotenv = [ {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, diff --git a/pyproject.toml b/pyproject.toml index 94ccef10..3698056b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ packages = [{ include = "modmail" }] [tool.poetry.dependencies] python = "^3.8" aiohttp = { extras = ["speedups"], version = "^3.7.4" } +arrow = "^1.1.1" colorama = "^0.4.3" coloredlogs = "^15.0" "discord.py" = {git = "https://github.com/Rapptz/discord.py.git", rev = "master"} diff --git a/requirements.txt b/requirements.txt index 5ae4032a..630fbf40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ aiodns==3.0.0; python_version >= "3.6" and python_full_version >= "3.8.0" aiohttp==3.7.4.post0; python_version >= "3.6" +arrow==1.1.1; python_version >= "3.6" async-timeout==3.0.1; python_full_version >= "3.8.0" and python_version >= "3.6" attrs==21.2.0; python_full_version >= "3.8.0" and python_version >= "3.6" brotlipy==0.7.0; python_version >= "3.6" and python_full_version >= "3.8.0" @@ -19,7 +20,9 @@ pycares==4.0.0; python_version >= "3.6" pycparser==2.20; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" pydantic==1.8.2; python_full_version >= "3.6.1" pyreadline==2.1; python_version >= "2.7" and python_full_version < "3.0.0" and sys_platform == "win32" or python_full_version >= "3.5.0" and sys_platform == "win32" +python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" python-dotenv==0.17.1 +six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") typing-extensions==3.10.0.0; python_version >= "3.6" and python_full_version >= "3.8.0" yarl==1.6.3; python_version >= "3.6" and python_full_version >= "3.8.0" From bf450339ac064ed75811b6c0b3cbb9b67188b854 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 4 Aug 2021 18:52:30 -0400 Subject: [PATCH 006/169] chore: update docstring --- modmail/exts/meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/exts/meta.py b/modmail/exts/meta.py index 199ced69..f3ecda97 100644 --- a/modmail/exts/meta.py +++ b/modmail/exts/meta.py @@ -10,7 +10,7 @@ class Meta(commands.Cog): - """The description for Ping goes here.""" + """Meta commands to get info about the bot itself.""" def __init__(self, bot: ModmailBot): self.bot = bot From 01d3dc0d5dfe550b665805f416a5bb5f1ff57529 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 4 Aug 2021 23:28:31 -0400 Subject: [PATCH 007/169] feat: add cog metadata and loading rules cogs will now load or not load depending on current bot mode. - all cogs now require a variable named "COG_METADATA" which is an instance of "CogMetadata" from "modmail.utils.cogs" - if a cog does not have it a warning will be logged and will assume that the cog should be loaded. - there are two modes, development mode and plugin dev mode - development mode can be set with DEVEL=True in environment variables - plugin dev mode can be set with PLUGIN_DEV=True - by defalt, both modes are false. --- modmail/config.py | 3 +++ modmail/exts/meta.py | 2 ++ modmail/exts/utils/extensions.py | 3 +++ modmail/utils/cogs.py | 36 ++++++++++++++++++++++++++++++++ modmail/utils/extensions.py | 29 +++++++++++++++++++++++++ 5 files changed, 73 insertions(+) create mode 100644 modmail/utils/cogs.py diff --git a/modmail/config.py b/modmail/config.py index 2dfac8a8..8238f167 100644 --- a/modmail/config.py +++ b/modmail/config.py @@ -152,6 +152,9 @@ class DevConfig(BaseSettings): """ log_level: conint(ge=0, le=50) = getattr(logging, "NOTICE", 25) + production: bool = True + devel: bool = False + plugin_dev: bool = False class EmojiConfig(BaseSettings): diff --git a/modmail/exts/meta.py b/modmail/exts/meta.py index f3ecda97..767cfc8c 100644 --- a/modmail/exts/meta.py +++ b/modmail/exts/meta.py @@ -5,8 +5,10 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger +from modmail.utils.cogs import CogMetadata log: ModmailLogger = logging.getLogger(__name__) +COG_METADATA = CogMetadata() class Meta(commands.Cog): diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/utils/extensions.py index a0fdf4de..55e972d5 100644 --- a/modmail/exts/utils/extensions.py +++ b/modmail/exts/utils/extensions.py @@ -13,6 +13,7 @@ from modmail import exts from modmail.bot import ModmailBot from modmail.log import ModmailLogger +from modmail.utils.cogs import CogMetadata from modmail.utils.extensions import EXTENSIONS, unqualify log: ModmailLogger = logging.getLogger(__name__) @@ -23,6 +24,8 @@ } BASE_PATH_LEN = len(exts.__name__.split(".")) +COG_METADATA = CogMetadata(devel=True) + class Action(Enum): """Represents an action to perform on an extension.""" diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py new file mode 100644 index 00000000..dde438f5 --- /dev/null +++ b/modmail/utils/cogs.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from enum import IntEnum + + +class BotModes(IntEnum): + """ + Valid modes for the bot. + + These values affect logging levels, which logs are loaded, and so forth. + """ + + production = int("1", 2) + devel = int("10", 2) + plugin_dev = int("100", 2) + + +BOT_MODES = BotModes + + +@dataclass() +class CogMetadata: + """Cog metadata class to determine if cog should load at runtime depending on bot configuration.""" + + # load if bot is in development mode + # development mode is when the bot has its metacogs loaded, like the eval and extension cogs + devel: bool = False + # plugin development mode + # used for loading bot plugins that help with plugin debugging + plugin_dev: bool = False + + +def calc_mode(metadata: CogMetadata) -> int: + """Calculate the combination of different variables and return the binary combination.""" + mode = int(metadata.devel << 1) or 0 + mode = mode + (int(metadata.plugin_dev) << 2) + return mode diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 24f4593b..4c5de098 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -3,10 +3,20 @@ # MIT License 2021 Python Discord import importlib import inspect +import logging import pkgutil from typing import Iterator, NoReturn from modmail import exts +from modmail.config import CONFIG +from modmail.log import ModmailLogger +from modmail.utils.cogs import BOT_MODES, calc_mode + +BOT_MODE = calc_mode(CONFIG.dev) +log: ModmailLogger = logging.getLogger(__name__) +log.trace(f"BOT_MODE value: {BOT_MODE}") +log.debug(f"Dev mode status: {bool(BOT_MODE & BOT_MODES.devel)}") +log.debug(f"Plugin dev mode status: {bool(BOT_MODE & BOT_MODES.plugin_dev)}") def unqualify(name: str) -> str: @@ -31,6 +41,25 @@ def on_error(name: str) -> NoReturn: # If it lacks a setup function, it's not an extension. continue + if module.name.endswith("utils.extensions"): + # due to circular imports, the utils.extensions cog is not able to utilize the cog metadata class + # it is hardcoded here as a dev cog in order to prevent it from causing bugs + if BOT_MODE & BOT_MODES.devel: + yield module.name + continue + log.debug(module.name) + + imported = importlib.import_module(module.name) + if (cog_metadata := getattr(imported, "COG_METADATA", None)) is not None: + # check if this cog is dev only or plugin dev only + load_cog = bool(((cog_metadata.devel << 1) + (cog_metadata.plugin_dev << 2)) & BOT_MODE) + log.trace(f"Load cog {module.name!r}?: {load_cog}") + if load_cog: + yield module.name + continue + + log.warn(f"Cog {module.name!r} is missing a COG_METADATA variable. Assuming its a normal cog.") + yield module.name From 47343c88288316592206be3acae21a51a87b252b Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 01:17:02 -0400 Subject: [PATCH 008/169] chore: colorize trace logging level --- modmail/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index 3a9076e2..f0eaf598 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -5,7 +5,7 @@ import coloredlogs -from .log import ModmailLogger +from modmail.log import ModmailLogger if TYPE_CHECKING: from modmail.bot import ModmailBot @@ -42,11 +42,11 @@ file_handler.setLevel(logging.TRACE) -coloredlogs.install( - level=logging.TRACE, - fmt=fmt, - datefmt=datefmt, -) +# configure trace color +_LEVEL_STYLES = dict(coloredlogs.DEFAULT_LEVEL_STYLES) +_LEVEL_STYLES["trace"] = _LEVEL_STYLES["spam"] + +coloredlogs.install(level=logging.TRACE, fmt=fmt, datefmt=datefmt, level_styles=_LEVEL_STYLES) # Create root logger root: ModmailLogger = logging.getLogger() @@ -59,6 +59,5 @@ # Set asyncio logging back to the default of INFO even if asyncio's debug mode is enabled. logging.getLogger("asyncio").setLevel(logging.INFO) -root.debug("Logging initialization complete") instance: "ModmailBot" = None # Global ModmailBot instance. From 01d6e96774adbc2cebcf096ffbf3a069140aed31 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 03:51:50 -0400 Subject: [PATCH 009/169] chore: make cogs load when they are supposed to cogs that load all the time were not loading all of the time, specifically when no mode was explicitly set. --- modmail/bot.py | 5 +++-- modmail/config.py | 2 +- modmail/exts/meta.py | 2 -- modmail/exts/utils/extensions.py | 19 +++++++++++++------ modmail/utils/cogs.py | 10 ++++++---- modmail/utils/extensions.py | 15 ++++++--------- 6 files changed, 29 insertions(+), 24 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 1775b06f..bd942523 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -62,8 +62,9 @@ def load_extensions(self) -> None: extensions = set(EXTENSIONS) # Create a mutable copy. - for extension in extensions: - self.load_extension(extension) + for extension, should_load in extensions: + if should_load: + self.load_extension(extension) def add_cog(self, cog: commands.Cog) -> None: """ diff --git a/modmail/config.py b/modmail/config.py index 8238f167..5902ce75 100644 --- a/modmail/config.py +++ b/modmail/config.py @@ -153,7 +153,7 @@ class DevConfig(BaseSettings): log_level: conint(ge=0, le=50) = getattr(logging, "NOTICE", 25) production: bool = True - devel: bool = False + develop: bool = False plugin_dev: bool = False diff --git a/modmail/exts/meta.py b/modmail/exts/meta.py index 767cfc8c..f3ecda97 100644 --- a/modmail/exts/meta.py +++ b/modmail/exts/meta.py @@ -5,10 +5,8 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import CogMetadata log: ModmailLogger = logging.getLogger(__name__) -COG_METADATA = CogMetadata() class Meta(commands.Cog): diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/utils/extensions.py index 55e972d5..ec903df4 100644 --- a/modmail/exts/utils/extensions.py +++ b/modmail/exts/utils/extensions.py @@ -24,7 +24,7 @@ } BASE_PATH_LEN = len(exts.__name__.split(".")) -COG_METADATA = CogMetadata(devel=True) +COG_METADATA = CogMetadata(develop=True) class Action(Enum): @@ -50,14 +50,17 @@ async def convert(self, ctx: Context, argument: str) -> str: return argument argument = argument.lower() + extensions = [] + for ext, _nul in EXTENSIONS: + extensions.append(ext) - if argument in EXTENSIONS: + if argument in extensions: return argument elif (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS: return qualified_arg matches = [] - for ext in EXTENSIONS: + for ext in extensions: if argument == unqualify(ext): matches.append(ext) @@ -140,7 +143,9 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: return if "**" in extensions: - extensions = EXTENSIONS + extensions = [] + for ext, _nul in EXTENSIONS: + extensions.append(ext) elif "*" in extensions: extensions = set(self.bot.extensions.keys()) | set(extensions) extensions.remove("*") @@ -178,8 +183,10 @@ async def list_command(self, ctx: Context) -> None: def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" categories = {} - - for ext in EXTENSIONS: + extensions = [] + for ext, _nul in EXTENSIONS: + extensions.append(ext) + for ext in extensions: if ext in self.bot.extensions: status = ":thumbsup:" else: diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index dde438f5..ca123697 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -10,7 +10,7 @@ class BotModes(IntEnum): """ production = int("1", 2) - devel = int("10", 2) + develop = int("10", 2) plugin_dev = int("100", 2) @@ -23,7 +23,8 @@ class CogMetadata: # load if bot is in development mode # development mode is when the bot has its metacogs loaded, like the eval and extension cogs - devel: bool = False + production: bool = False + develop: bool = False # plugin development mode # used for loading bot plugins that help with plugin debugging plugin_dev: bool = False @@ -31,6 +32,7 @@ class CogMetadata: def calc_mode(metadata: CogMetadata) -> int: """Calculate the combination of different variables and return the binary combination.""" - mode = int(metadata.devel << 1) or 0 - mode = mode + (int(metadata.plugin_dev) << 2) + mode = int(getattr(metadata, "production", False)) + mode = mode + int(getattr(metadata, "develop", False) << 1) or 0 + mode = mode + (int(getattr(metadata, "plugin_dev", False)) << 2) return mode diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 4c5de098..887efdc6 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -15,7 +15,7 @@ BOT_MODE = calc_mode(CONFIG.dev) log: ModmailLogger = logging.getLogger(__name__) log.trace(f"BOT_MODE value: {BOT_MODE}") -log.debug(f"Dev mode status: {bool(BOT_MODE & BOT_MODES.devel)}") +log.debug(f"Dev mode status: {bool(BOT_MODE & BOT_MODES.develop)}") log.debug(f"Plugin dev mode status: {bool(BOT_MODE & BOT_MODES.plugin_dev)}") @@ -44,23 +44,20 @@ def on_error(name: str) -> NoReturn: if module.name.endswith("utils.extensions"): # due to circular imports, the utils.extensions cog is not able to utilize the cog metadata class # it is hardcoded here as a dev cog in order to prevent it from causing bugs - if BOT_MODE & BOT_MODES.devel: - yield module.name + yield module.name, BOT_MODES.develop & BOT_MODE continue - log.debug(module.name) imported = importlib.import_module(module.name) if (cog_metadata := getattr(imported, "COG_METADATA", None)) is not None: # check if this cog is dev only or plugin dev only - load_cog = bool(((cog_metadata.devel << 1) + (cog_metadata.plugin_dev << 2)) & BOT_MODE) + load_cog = bool(calc_mode(cog_metadata) & BOT_MODE) log.trace(f"Load cog {module.name!r}?: {load_cog}") - if load_cog: - yield module.name + yield module.name, load_cog continue - log.warn(f"Cog {module.name!r} is missing a COG_METADATA variable. Assuming its a normal cog.") + log.notice(f"Cog {module.name!r} is missing a COG_METADATA variable. Assuming its a normal cog.") - yield module.name + yield (module.name, True) EXTENSIONS = frozenset(walk_extensions()) From 3ea01220323b97d9125dfa26d02bac53b0e9fb79 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 13:13:32 -0400 Subject: [PATCH 010/169] chore: use hidden variables, correct typing --- modmail/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index f0eaf598..5eb488f4 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -1,5 +1,6 @@ import logging import logging.handlers +import typing from pathlib import Path from typing import TYPE_CHECKING @@ -16,8 +17,8 @@ logging.addLevelName(logging.NOTICE, "NOTICE") LOG_LEVEL = 5 -fmt = "%(asctime)s %(levelname)10s %(name)15s - [%(lineno)5d]: %(message)s" -datefmt = "%Y/%m/%d %H:%M:%S" +_FMT = "%(asctime)s %(levelname)10s %(name)15s - [%(lineno)5d]: %(message)s" +_DATEFMT = "%Y/%m/%d %H:%M:%S" logging.setLoggerClass(ModmailLogger) @@ -35,8 +36,8 @@ file_handler.setFormatter( logging.Formatter( - fmt=fmt, - datefmt=datefmt, + fmt=_FMT, + datefmt=_DATEFMT, ) ) @@ -46,7 +47,7 @@ _LEVEL_STYLES = dict(coloredlogs.DEFAULT_LEVEL_STYLES) _LEVEL_STYLES["trace"] = _LEVEL_STYLES["spam"] -coloredlogs.install(level=logging.TRACE, fmt=fmt, datefmt=datefmt, level_styles=_LEVEL_STYLES) +coloredlogs.install(level=logging.TRACE, fmt=_FMT, datefmt=_DATEFMT, level_styles=_LEVEL_STYLES) # Create root logger root: ModmailLogger = logging.getLogger() @@ -60,4 +61,4 @@ logging.getLogger("asyncio").setLevel(logging.INFO) -instance: "ModmailBot" = None # Global ModmailBot instance. +instance: typing.Optional["ModmailBot"] = None # Global ModmailBot instance. From ff61dadf0c83be978f57dfb778b110c38c4aa9ed Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 13:14:07 -0400 Subject: [PATCH 011/169] chore: put comments in correct place --- modmail/utils/cogs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index ca123697..e9b71aa0 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -21,9 +21,11 @@ class BotModes(IntEnum): class CogMetadata: """Cog metadata class to determine if cog should load at runtime depending on bot configuration.""" + # prod mode + # set this to true if the cog should always load + production: bool = False # load if bot is in development mode # development mode is when the bot has its metacogs loaded, like the eval and extension cogs - production: bool = False develop: bool = False # plugin development mode # used for loading bot plugins that help with plugin debugging From dec1f6841da653b2c05fc4eb7aca7d5d13c70fc2 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 13:44:49 -0400 Subject: [PATCH 012/169] chore: use absolute imports --- modmail/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/__main__.py b/modmail/__main__.py index 54b4d33c..fc9592ad 100644 --- a/modmail/__main__.py +++ b/modmail/__main__.py @@ -8,7 +8,7 @@ except ImportError: pass -from .bot import ModmailBot +from modmail.bot import ModmailBot log = logging.getLogger(__name__) From 57ad0dd1788e9f15bea47686bd57b6a4a12d93a6 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 13:50:17 -0400 Subject: [PATCH 013/169] chore: update dependencies --- poetry.lock | 65 ++++++++++++++---------------------------------- requirements.txt | 18 +++++++------- 2 files changed, 28 insertions(+), 55 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3a54b0e2..d92895c9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -464,7 +464,7 @@ pyreadline = {version = "*", markers = "sys_platform == \"win32\""} [[package]] name = "identify" -version = "2.2.11" +version = "2.2.12" description = "File identification library for Python" category = "dev" optional = false @@ -858,7 +858,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "regex" -version = "2021.7.6" +version = "2021.8.3" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -1015,7 +1015,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "2172524caa421ed11d12e95572a62ed432b8a295022440e94f4f480c917e7bc7" +content-hash = "6735433ebc54f95f08d6b83877c0fee831ea13af835a5e4cfaf3310d41f83157" [metadata.files] aiodns = [ @@ -1361,8 +1361,8 @@ humanfriendly = [ {file = "humanfriendly-9.2.tar.gz", hash = "sha256:f7dba53ac7935fd0b4a2fc9a29e316ddd9ea135fb3052d3d0279d10c18ff9c48"}, ] identify = [ - {file = "identify-2.2.11-py2.py3-none-any.whl", hash = "sha256:7abaecbb414e385752e8ce02d8c494f4fbc780c975074b46172598a28f1ab839"}, - {file = "identify-2.2.11.tar.gz", hash = "sha256:a0e700637abcbd1caae58e0463861250095dfe330a8371733a471af706a4a29a"}, + {file = "identify-2.2.12-py2.py3-none-any.whl", hash = "sha256:a510cbe155f39665625c8a4c4b4f9360cbce539f51f23f47836ab7dd852db541"}, + {file = "identify-2.2.12.tar.gz", hash = "sha256:242332b3bdd45a8af1752d5d5a3afb12bee26f8e67c4be06e394f82d05ef1a4d"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, @@ -1644,47 +1644,20 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] regex = [ - {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, - {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, - {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, - {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, - {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, - {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, - {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, - {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, - {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, - {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, - {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, - {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, - {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"}, + {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, diff --git a/requirements.txt b/requirements.txt index 630fbf40..42629b9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,21 @@ # Do not manually edit. # Generate with "poetry run task export" -aiodns==3.0.0; python_version >= "3.6" and python_full_version >= "3.8.0" +aiodns==3.0.0; python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" aiohttp==3.7.4.post0; python_version >= "3.6" arrow==1.1.1; python_version >= "3.6" -async-timeout==3.0.1; python_full_version >= "3.8.0" and python_version >= "3.6" -attrs==21.2.0; python_full_version >= "3.8.0" and python_version >= "3.6" -brotlipy==0.7.0; python_version >= "3.6" and python_full_version >= "3.8.0" -cchardet==2.1.7; python_version >= "3.6" and python_full_version >= "3.8.0" +async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" +attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" +brotlipy==0.7.0; python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" +cchardet==2.1.7; python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" cffi==1.14.6; python_version >= "3.6" -chardet==4.0.0; python_full_version >= "3.8.0" and python_version >= "3.6" +chardet==4.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.8.0" colorama==0.4.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") coloredlogs==15.0.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") discord.py @ git+https://github.com/Rapptz/discord.py.git@master ; python_full_version >= "3.8.0" humanfriendly==9.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" idna==3.2; python_version >= "3.6" -multidict==5.1.0; python_version >= "3.6" and python_full_version >= "3.8.0" +multidict==5.1.0; python_version >= "3.6" and python_full_version >= "3.8.0" or python_version >= "3.6" pycares==4.0.0; python_version >= "3.6" pycparser==2.20; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" pydantic==1.8.2; python_full_version >= "3.6.1" @@ -24,5 +24,5 @@ python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0 python-dotenv==0.17.1 six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") -typing-extensions==3.10.0.0; python_version >= "3.6" and python_full_version >= "3.8.0" -yarl==1.6.3; python_version >= "3.6" and python_full_version >= "3.8.0" +typing-extensions==3.10.0.0; python_version >= "3.6" or python_full_version >= "3.6.1" or python_version >= "3.6" and python_full_version >= "3.8.0" +yarl==1.6.3; python_version >= "3.6" and python_full_version >= "3.8.0" or python_version >= "3.6" From f2332e6adffb04b677c4f2b771d68568e50726ed Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 14:21:08 -0400 Subject: [PATCH 014/169] feat: add get prefix command --- modmail/exts/meta.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modmail/exts/meta.py b/modmail/exts/meta.py index f3ecda97..ef4b5b3a 100644 --- a/modmail/exts/meta.py +++ b/modmail/exts/meta.py @@ -26,6 +26,11 @@ async def uptime(self, ctx: commands.Context) -> None: timestamp = self.bot.start_time.format("X").split(".")[0] await ctx.send(f"Start time: ") + @commands.command(name="prefix") + async def prefix(self, ctx: commands.Context) -> None: + """Return the configured prefix.""" + await ctx.send(f"My current prefix is `{self.bot.config.bot.prefix}`") + def setup(bot: ModmailBot) -> None: """Load the Meta cog.""" From 4763d15941297222b8ce12253fd29bef5a9c4006 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 15:54:31 -0400 Subject: [PATCH 015/169] chore: add pong as a ping alias and update doc --- modmail/exts/meta.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/exts/meta.py b/modmail/exts/meta.py index ef4b5b3a..cec8ea68 100644 --- a/modmail/exts/meta.py +++ b/modmail/exts/meta.py @@ -15,9 +15,9 @@ class Meta(commands.Cog): def __init__(self, bot: ModmailBot): self.bot = bot - @commands.command() + @commands.command(name="ping", aliases=("pong",)) async def ping(self, ctx: Context) -> None: - """Check response time.""" + """Ping the bot to see its latency and state.""" await ctx.send(f"{round(self.bot.latency * 1000)}ms") @commands.command(name="uptime") From 13bacc012b456e47a9bf510513e77617c3acc37a Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 15:55:07 -0400 Subject: [PATCH 016/169] chore: use discord py method for mention prefix --- modmail/bot.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index bd942523..341203ba 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -1,9 +1,7 @@ import asyncio import logging -import typing as t import arrow -import discord from aiohttp import ClientSession from discord.ext import commands @@ -26,16 +24,12 @@ def __init__(self, **kwargs): self.internal = INTERNAL self.http_session: ClientSession = None self.start_time = arrow.utcnow() - super().__init__(command_prefix=self.get_prefix, **kwargs) + super().__init__(command_prefix=commands.when_mentioned_or(self.config.bot.prefix), **kwargs) async def create_session(self) -> None: """Create an aiohttp client session.""" self.http_session = ClientSession() - async def get_prefix(self, message: discord.Message = None) -> t.List[str]: - """Returns the bot prefix, but also allows the bot to work with user mentions.""" - return [self.config.bot.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] - async def close(self) -> None: """Safely close HTTP session and extensions when bot is shutting down.""" for ext in list(self.extensions): From 3137be1fead3d6f77584f1a73433a84974286431 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 17:41:39 -0400 Subject: [PATCH 017/169] chore: remove dupe assigning in utils.extensions --- modmail/utils/extensions.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 887efdc6..63549516 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -35,12 +35,6 @@ def on_error(name: str) -> NoReturn: # Ignore module/package names starting with an underscore. continue - if module.ispkg: - imported = importlib.import_module(module.name) - if not inspect.isfunction(getattr(imported, "setup", None)): - # If it lacks a setup function, it's not an extension. - continue - if module.name.endswith("utils.extensions"): # due to circular imports, the utils.extensions cog is not able to utilize the cog metadata class # it is hardcoded here as a dev cog in order to prevent it from causing bugs @@ -48,6 +42,11 @@ def on_error(name: str) -> NoReturn: continue imported = importlib.import_module(module.name) + if module.ispkg: + if not inspect.isfunction(getattr(imported, "setup", None)): + # If it lacks a setup function, it's not an extension. + continue + if (cog_metadata := getattr(imported, "COG_METADATA", None)) is not None: # check if this cog is dev only or plugin dev only load_cog = bool(calc_mode(cog_metadata) & BOT_MODE) From 5d5fd393b75bc115498b2aaf3c3360f74b12cc72 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 19:02:47 -0400 Subject: [PATCH 018/169] chore: rename cogmetadata to extmetadata --- modmail/exts/utils/extensions.py | 4 ++-- modmail/utils/cogs.py | 6 +++--- modmail/utils/extensions.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/utils/extensions.py index ec903df4..6e0907f8 100644 --- a/modmail/exts/utils/extensions.py +++ b/modmail/exts/utils/extensions.py @@ -13,7 +13,7 @@ from modmail import exts from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import CogMetadata +from modmail.utils.cogs import ExtMetadata from modmail.utils.extensions import EXTENSIONS, unqualify log: ModmailLogger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ } BASE_PATH_LEN = len(exts.__name__.split(".")) -COG_METADATA = CogMetadata(develop=True) +EXT_METADATA = ExtMetadata(develop=True) class Action(Enum): diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index e9b71aa0..8fb04005 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -18,8 +18,8 @@ class BotModes(IntEnum): @dataclass() -class CogMetadata: - """Cog metadata class to determine if cog should load at runtime depending on bot configuration.""" +class ExtMetadata: + """Ext metadata class to determine if extension should load at runtime depending on bot configuration.""" # prod mode # set this to true if the cog should always load @@ -32,7 +32,7 @@ class CogMetadata: plugin_dev: bool = False -def calc_mode(metadata: CogMetadata) -> int: +def calc_mode(metadata: ExtMetadata) -> int: """Calculate the combination of different variables and return the binary combination.""" mode = int(getattr(metadata, "production", False)) mode = mode + int(getattr(metadata, "develop", False) << 1) or 0 diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 63549516..9579914a 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -47,14 +47,14 @@ def on_error(name: str) -> NoReturn: # If it lacks a setup function, it's not an extension. continue - if (cog_metadata := getattr(imported, "COG_METADATA", None)) is not None: + if (ext_metadata := getattr(imported, "EXT_METADATA", None)) is not None: # check if this cog is dev only or plugin dev only - load_cog = bool(calc_mode(cog_metadata) & BOT_MODE) + load_cog = bool(calc_mode(ext_metadata) & BOT_MODE) log.trace(f"Load cog {module.name!r}?: {load_cog}") yield module.name, load_cog continue - log.notice(f"Cog {module.name!r} is missing a COG_METADATA variable. Assuming its a normal cog.") + log.notice(f"Cog {module.name!r} is missing an EXT_METADATA variable. Assuming its a normal cog.") yield (module.name, True) From e1bd635b77f9cca2967711a7842b6e3dbd63c504 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 19:19:00 -0400 Subject: [PATCH 019/169] feat: add plugin loading system and helper cmds --- modmail/exts/utils/plugin_manager.py | 271 +++++++++++++++++++++++++++ modmail/plugins/.gitignore | 5 + modmail/plugins/local/readme.md | 1 + modmail/utils/plugin_manager.py | 67 +++++++ 4 files changed, 344 insertions(+) create mode 100644 modmail/exts/utils/plugin_manager.py create mode 100644 modmail/plugins/.gitignore create mode 100644 modmail/plugins/local/readme.md create mode 100644 modmail/utils/plugin_manager.py diff --git a/modmail/exts/utils/plugin_manager.py b/modmail/exts/utils/plugin_manager.py new file mode 100644 index 00000000..a5652f12 --- /dev/null +++ b/modmail/exts/utils/plugin_manager.py @@ -0,0 +1,271 @@ +# original source: +# https://github.com/python-discord/bot/blob/a8869b4d60512b173871c886321b261cbc4acca9/bot/exts/utils/extensions.py # noqa: E501 +# MIT License 2021 Python Discord +import functools +import logging +import typing as t +from enum import Enum +from pathlib import Path + +from discord import Colour, Embed +from discord.ext import commands +from discord.ext.commands import Context, group + +from modmail import plugins +from modmail.bot import ModmailBot +from modmail.log import ModmailLogger +from modmail.utils.cogs import ExtMetadata +from modmail.utils.plugin_manager import PLUGINS, unqualify + +log: ModmailLogger = logging.getLogger(__name__) + +BASE_PATH = Path(plugins.__file__).parent +BASE_PATH_LEN = len(plugins.__name__.split(".")) + +EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) + + +class Action(Enum): + """Represents an action to perform on an extension.""" + + # Need to be partial otherwise they are considered to be function definitions. + LOAD = functools.partial(ModmailBot.load_extension) + UNLOAD = functools.partial(ModmailBot.unload_extension) + RELOAD = functools.partial(ModmailBot.reload_extension) + + +class Plugin(commands.Converter): + """ + Fully qualify the name of an extension and ensure it exists. + + The * and ** values bypass this when used with the reload command. + """ + + async def convert(self, ctx: Context, argument: str) -> str: + """Fully qualify the name of an plugin and ensure it exists.""" + # Special values to reload all plugins + if argument == "*" or argument == "**": + return argument + + argument = argument.lower() + plugs = [] + for ext, _nul in PLUGINS: + plugs.append(ext) + + if argument in plugs: + return argument + elif (qualified_arg := f"{plugins.__name__}.{argument}") in PLUGINS: + return qualified_arg + + matches = [] + for ext in plugs: + if argument == unqualify(ext): + matches.append(ext) + + if len(matches) > 1: + matches.sort() + names = "\n".join(matches) + raise commands.BadArgument( + f":x: `{argument}` is an ambiguous extension name. " + f"Please use one of the following fully-qualified names.```\n{names}```" + ) + elif matches: + return matches[0] + else: + raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") + + +class Plugin_Manager(commands.Cog, name="Plugin Manager"): + """Extension management commands.""" + + def __init__(self, bot: ModmailBot): + self.bot = bot + + @group(name="plugins", aliases=("plug", "plugs"), invoke_without_command=True) + async def extensions_group(self, ctx: Context) -> None: + """Load, unload, reload, and list loaded extensions.""" + await ctx.send_help(ctx.command) + + @extensions_group.command(name="load", aliases=("l",)) + async def load_command(self, ctx: Context, *plugs: Plugin) -> None: + r""" + Load extensions given their fully qualified or unqualified names. + + If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. + """ # noqa: W605 + if not plugs: + await ctx.send_help(ctx.command) + return + + if "*" in plugs or "**" in plugs: + plugs = set(PLUGINS) - set(self.bot.extensions.keys()) + + msg = self.batch_manage(Action.LOAD, *plugs) + await ctx.send(msg) + + @extensions_group.command(name="unload", aliases=("ul",)) + async def unload_command(self, ctx: Context, *plugs: Plugin) -> None: + r""" + Unload currently loaded extensions given their fully qualified or unqualified names. + + If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. + """ # noqa: W605 + if not plugs: + await ctx.send_help(ctx.command) + return + + if "*" in plugs or "**" in plugs: + plugs = set(self.bot.extensions.keys()) + + msg = self.batch_manage(Action.UNLOAD, *plugs) + + await ctx.send(msg) + + @extensions_group.command(name="reload", aliases=("r",)) + async def reload_command(self, ctx: Context, *plugs: Plugin) -> None: + r""" + Reload extensions given their fully qualified or unqualified names. + + If an extension fails to be reloaded, it will be rolled-back to the prior working state. + + If '\*' is given as the name, all currently loaded extensions will be reloaded. + If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. + """ # noqa: W605 + if not plugs: + await ctx.send_help(ctx.command) + return + + if "**" in plugs: + plugs = [] + for plug, _nul in PLUGINS: + plugs.append(plug) + elif "*" in plugs: + plugs = set(self.bot.extensions.keys()) | set(plugs) + plugs.remove("*") + + msg = self.batch_manage(Action.RELOAD, *plugs) + + await ctx.send(msg) + + @extensions_group.command(name="list", aliases=("all",)) + async def list_command(self, ctx: Context) -> None: + """ + Get a list of all extensions, including their loaded status. + + Grey indicates that the extension is unloaded. + Green indicates that the extension is currently loaded. + """ + embed = Embed(colour=Colour.blurple()) + embed.set_author( + name="Plugins List", + ) + + lines = [] + categories = self.group_extension_statuses() + for category, plugs in sorted(categories.items()): + # Treat each category as a single line by concatenating everything. + # This ensures the paginator will not cut off a page in the middle of a category. + category = category.replace("_", " ").title() + plugs = "\n".join(sorted(plugs)) + lines.append(f"**{category}**\n{plugs}\n") + + log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") + # await Paginator.paginate(lines, ctx, embed, scale_to_size=700, empty=False) + await ctx.send(lines) + + def group_extension_statuses(self) -> t.Mapping[str, str]: + """Return a mapping of extension names and statuses to their categories.""" + categories = {} + plugs = [] + for ext, _nul in PLUGINS: + plugs.append(ext) + for plug in plugs: + if plug in self.bot.extensions: + status = ":thumbsup:" + else: + status = ":thumbsdown:" + + path = ext.split(".") + if len(path) > BASE_PATH_LEN + 1: + category = " - ".join(path[BASE_PATH_LEN:-1]) + else: + category = "uncategorised" + + categories.setdefault(category, []).append(f"{status} {path[-1]}") + + return categories + + def batch_manage(self, action: Action, *plugs: str) -> str: + """ + Apply an action to multiple extensions and return a message with the results. + + If only one extension is given, it is deferred to `manage()`. + """ + if len(plugs) == 1: + msg, _ = self.manage(action, plugs[0]) + return msg + + verb = action.name.lower() + failures = {} + + for plug in plugs: + _, error = self.manage(action, plug) + if error: + failures[plug] = error + + emoji = ":x:" if failures else ":ok_hand:" + msg = f"{emoji} {len(plugs) - len(failures)} / {len(plugs)} plugins {verb}ed." + + if failures: + failures = "\n".join(f"{ext}\n {err}" for ext, err in failures.items()) + msg += f"\nFailures:```\n{failures}```" + + log.debug(f"Batch {verb}ed plugins.") + + return msg + + def manage(self, action: Action, plug: str) -> t.Tuple[str, t.Optional[str]]: + """Apply an action to an plugin and return the status message and any error message.""" + verb = action.name.lower() + error_msg = None + + try: + action.value(self.bot, plug) + except (commands.ExtensionAlreadyLoaded, commands.ExtensionNotLoaded): + if action is Action.RELOAD: + # When reloading, just load the plugin if it was not loaded. + log.debug("Treating {plug!r} as if it was not loaded.") + return self.manage(Action.LOAD, plug) + + msg = f":x: Plugin `{plug}` is already {verb}ed." + log.debug(msg[4:]) + except Exception as e: + if hasattr(e, "original"): + e = e.original + + log.exception(f"Plugin '{plug}' failed to {verb}.") + + error_msg = f"{e.__class__.__name__}: {e}" + msg = f":x: Failed to {verb} plugin `{plug}`:\n```\n{error_msg}```" + else: + msg = f":ok_hand: Plugin successfully {verb}ed: `{plug}`." + log.debug(msg[10:]) + + return msg, error_msg + + # This cannot be static (must have a __func__ attribute). + async def cog_check(self, ctx: Context) -> bool: + """Only allow bot owners to invoke the commands in this cog.""" + return await self.bot.is_owner(ctx.author) + + # This cannot be static (must have a __func__ attribute). + async def cog_command_error(self, ctx: Context, error: Exception) -> None: + """Handle BadArgument errors locally to prevent the help command from showing.""" + if isinstance(error, commands.BadArgument): + await ctx.send(str(error)) + error.handled = True + + +def setup(bot: ModmailBot) -> None: + """Load the Plugins manager cog.""" + bot.add_cog(Plugin_Manager(bot)) diff --git a/modmail/plugins/.gitignore b/modmail/plugins/.gitignore new file mode 100644 index 00000000..43ec4561 --- /dev/null +++ b/modmail/plugins/.gitignore @@ -0,0 +1,5 @@ +* +local/* +!.gitignore +!local +!local/readme.md diff --git a/modmail/plugins/local/readme.md b/modmail/plugins/local/readme.md new file mode 100644 index 00000000..acd4e373 --- /dev/null +++ b/modmail/plugins/local/readme.md @@ -0,0 +1 @@ +This folder is where local plugins can be put for developing. diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py new file mode 100644 index 00000000..b7cd2e20 --- /dev/null +++ b/modmail/utils/plugin_manager.py @@ -0,0 +1,67 @@ +# original source: +# https://github.com/python-discord/bot/blob/a8869b4d60512b173871c886321b261cbc4acca9/bot/utils/extensions.py +# MIT License 2021 Python Discord +""" +Helper utililites for managing plugins. + +TODO: Expand file to download extensions from github and gitlab from a list that is passed. +""" + + +import importlib +import importlib.util +import inspect +import logging +from pathlib import Path +from typing import Iterator + +from modmail import plugins +from modmail.config import CONFIG +from modmail.log import ModmailLogger +from modmail.utils.cogs import calc_mode + +BOT_MODE = calc_mode(CONFIG.dev) +BASE_PATH = Path(plugins.__file__).parent + + +log: ModmailLogger = logging.getLogger(__name__) +log.trace(f"BOT_MODE value: {BOT_MODE}") + + +def unqualify(name: str) -> str: + """Return an unqualified name given a qualified module/package `name`.""" + return name.rsplit(".", maxsplit=1)[-1] + + +def walk_plugins() -> Iterator[str]: + """Yield extension names from the modmail.plugins subpackage.""" + for path in BASE_PATH.glob("*/*.py"): + # calculate the module name, if it were to have a name from the path + relative_path = path.relative_to(BASE_PATH) + name = relative_path.__str__().rstrip(".py").replace("/", ".") + name = "modmail.plugins." + name + log.trace("Relative path: {0}".format(name)) + + spec = importlib.util.spec_from_file_location(name, path) + imported = importlib.util.module_from_spec(spec) + spec.loader.exec_module(imported) + + if not inspect.isfunction(getattr(imported, "setup", None)): + # If it lacks a setup function, it's not an extension. + continue + + if (ext_metadata := getattr(imported, "EXT_METADATA", None)) is not None: + # check if this plugin is dev only or plugin dev only + load_cog = bool(calc_mode(ext_metadata) & BOT_MODE) + log.trace(f"Load plugin {imported.__name__!r}?: {load_cog}") + yield imported.__name__, load_cog + continue + + log.notice( + f"Plugin {imported.__name__!r} is missing a EXT_METADATA variable. Assuming its a normal plugin." + ) + + yield (imported.__name__, True) + + +PLUGINS = frozenset(walk_plugins()) From 918c09f849db946861b00c71097360fe3f8b78db Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 19:24:23 -0400 Subject: [PATCH 020/169] chore: rename to PluginManager --- modmail/exts/utils/plugin_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/exts/utils/plugin_manager.py b/modmail/exts/utils/plugin_manager.py index a5652f12..04f7f8f0 100644 --- a/modmail/exts/utils/plugin_manager.py +++ b/modmail/exts/utils/plugin_manager.py @@ -75,7 +75,7 @@ async def convert(self, ctx: Context, argument: str) -> str: raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") -class Plugin_Manager(commands.Cog, name="Plugin Manager"): +class PluginManager(commands.Cog, name="Plugin Manager"): """Extension management commands.""" def __init__(self, bot: ModmailBot): @@ -268,4 +268,4 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: def setup(bot: ModmailBot) -> None: """Load the Plugins manager cog.""" - bot.add_cog(Plugin_Manager(bot)) + bot.add_cog(PluginManager(bot)) From 5565c0a4e9b124aa32f4d8193f96caab59b1712f Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 22:27:15 -0400 Subject: [PATCH 021/169] chore: prettify extension/plug list output --- modmail/exts/utils/extensions.py | 12 ++++++++---- modmail/exts/utils/plugin_manager.py | 13 +++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/utils/extensions.py index 6e0907f8..cf2318d3 100644 --- a/modmail/exts/utils/extensions.py +++ b/modmail/exts/utils/extensions.py @@ -177,8 +177,12 @@ async def list_command(self, ctx: Context) -> None: lines.append(f"**{category}**\n{extensions}\n") log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") - # await Paginator.paginate(lines, ctx, embed, scale_to_size=700, empty=False) - await ctx.send(lines) + + # since we currently don't have a paginator. + output = "" + for line in lines: + output += line + await ctx.send(output) def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" @@ -188,9 +192,9 @@ def group_extension_statuses(self) -> t.Mapping[str, str]: extensions.append(ext) for ext in extensions: if ext in self.bot.extensions: - status = ":thumbsup:" + status = ":green_circle:" else: - status = ":thumbsdown:" + status = ":red_circle:" path = ext.split(".") if len(path) > BASE_PATH_LEN + 1: diff --git a/modmail/exts/utils/plugin_manager.py b/modmail/exts/utils/plugin_manager.py index 04f7f8f0..af9f707d 100644 --- a/modmail/exts/utils/plugin_manager.py +++ b/modmail/exts/utils/plugin_manager.py @@ -169,9 +169,14 @@ async def list_command(self, ctx: Context) -> None: plugs = "\n".join(sorted(plugs)) lines.append(f"**{category}**\n{plugs}\n") - log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") + log.debug(f"{ctx.author} requested a list of all cogs. Returning a list.") # await Paginator.paginate(lines, ctx, embed, scale_to_size=700, empty=False) - await ctx.send(lines) + + # since we currently don't have a paginator. + output = "" + for line in lines: + output += line + await ctx.send(output) def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" @@ -181,9 +186,9 @@ def group_extension_statuses(self) -> t.Mapping[str, str]: plugs.append(ext) for plug in plugs: if plug in self.bot.extensions: - status = ":thumbsup:" + status = ":green_circle:" else: - status = ":thumbsdown:" + status = ":red_circle:" path = ext.split(".") if len(path) > BASE_PATH_LEN + 1: From 6cc5de6a2db9905ecca34d1d8552ba04c08563a2 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 23:01:52 -0400 Subject: [PATCH 022/169] chore: update var names and docstrings to plugins files were copied from the extension files as a base, but the docstrings were not updated. --- modmail/exts/utils/plugin_manager.py | 54 ++++++++++++++-------------- modmail/utils/plugin_manager.py | 6 ++-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/modmail/exts/utils/plugin_manager.py b/modmail/exts/utils/plugin_manager.py index af9f707d..31380702 100644 --- a/modmail/exts/utils/plugin_manager.py +++ b/modmail/exts/utils/plugin_manager.py @@ -26,7 +26,7 @@ class Action(Enum): - """Represents an action to perform on an extension.""" + """Represents an action to perform on an plugin.""" # Need to be partial otherwise they are considered to be function definitions. LOAD = functools.partial(ModmailBot.load_extension) @@ -36,7 +36,7 @@ class Action(Enum): class Plugin(commands.Converter): """ - Fully qualify the name of an extension and ensure it exists. + Fully qualify the name of an plugin and ensure it exists. The * and ** values bypass this when used with the reload command. """ @@ -66,32 +66,32 @@ async def convert(self, ctx: Context, argument: str) -> str: matches.sort() names = "\n".join(matches) raise commands.BadArgument( - f":x: `{argument}` is an ambiguous extension name. " + f":x: `{argument}` is an ambiguous plugin name. " f"Please use one of the following fully-qualified names.```\n{names}```" ) elif matches: return matches[0] else: - raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") + raise commands.BadArgument(f":x: Could not find the plugin `{argument}`.") class PluginManager(commands.Cog, name="Plugin Manager"): - """Extension management commands.""" + """Plugin management commands.""" def __init__(self, bot: ModmailBot): self.bot = bot @group(name="plugins", aliases=("plug", "plugs"), invoke_without_command=True) - async def extensions_group(self, ctx: Context) -> None: - """Load, unload, reload, and list loaded extensions.""" + async def plugins_group(self, ctx: Context) -> None: + """Load, unload, reload, and list loaded plugins.""" await ctx.send_help(ctx.command) - @extensions_group.command(name="load", aliases=("l",)) + @plugins_group.command(name="load", aliases=("l",)) async def load_command(self, ctx: Context, *plugs: Plugin) -> None: r""" - Load extensions given their fully qualified or unqualified names. + Load plugins given their fully qualified or unqualified names. - If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. + If '\*' or '\*\*' is given as the name, all unloaded plugins will be loaded. """ # noqa: W605 if not plugs: await ctx.send_help(ctx.command) @@ -103,12 +103,12 @@ async def load_command(self, ctx: Context, *plugs: Plugin) -> None: msg = self.batch_manage(Action.LOAD, *plugs) await ctx.send(msg) - @extensions_group.command(name="unload", aliases=("ul",)) + @plugins_group.command(name="unload", aliases=("ul",)) async def unload_command(self, ctx: Context, *plugs: Plugin) -> None: r""" - Unload currently loaded extensions given their fully qualified or unqualified names. + Unload currently loaded plugins given their fully qualified or unqualified names. - If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. + If '\*' or '\*\*' is given as the name, all loaded plugins will be unloaded. """ # noqa: W605 if not plugs: await ctx.send_help(ctx.command) @@ -121,15 +121,15 @@ async def unload_command(self, ctx: Context, *plugs: Plugin) -> None: await ctx.send(msg) - @extensions_group.command(name="reload", aliases=("r",)) + @plugins_group.command(name="reload", aliases=("r",)) async def reload_command(self, ctx: Context, *plugs: Plugin) -> None: r""" - Reload extensions given their fully qualified or unqualified names. + Reload plugins given their fully qualified or unqualified names. - If an extension fails to be reloaded, it will be rolled-back to the prior working state. + If an plugin fails to be reloaded, it will be rolled-back to the prior working state. - If '\*' is given as the name, all currently loaded extensions will be reloaded. - If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. + If '\*' is given as the name, all currently loaded plugins will be reloaded. + If '\*\*' is given as the name, all plugins, including unloaded ones, will be reloaded. """ # noqa: W605 if not plugs: await ctx.send_help(ctx.command) @@ -147,13 +147,13 @@ async def reload_command(self, ctx: Context, *plugs: Plugin) -> None: await ctx.send(msg) - @extensions_group.command(name="list", aliases=("all",)) + @plugins_group.command(name="list", aliases=("all",)) async def list_command(self, ctx: Context) -> None: """ - Get a list of all extensions, including their loaded status. + Get a list of all plugins, including their loaded status. - Grey indicates that the extension is unloaded. - Green indicates that the extension is currently loaded. + Grey indicates that the plugins is unloaded. + Green indicates that the plugins is currently loaded. """ embed = Embed(colour=Colour.blurple()) embed.set_author( @@ -161,7 +161,7 @@ async def list_command(self, ctx: Context) -> None: ) lines = [] - categories = self.group_extension_statuses() + categories = self.group_plugin_statuses() for category, plugs in sorted(categories.items()): # Treat each category as a single line by concatenating everything. # This ensures the paginator will not cut off a page in the middle of a category. @@ -178,8 +178,8 @@ async def list_command(self, ctx: Context) -> None: output += line await ctx.send(output) - def group_extension_statuses(self) -> t.Mapping[str, str]: - """Return a mapping of extension names and statuses to their categories.""" + def group_plugin_statuses(self) -> t.Mapping[str, str]: + """Return a mapping of plugin names and statuses to their categories.""" categories = {} plugs = [] for ext, _nul in PLUGINS: @@ -202,9 +202,9 @@ def group_extension_statuses(self) -> t.Mapping[str, str]: def batch_manage(self, action: Action, *plugs: str) -> str: """ - Apply an action to multiple extensions and return a message with the results. + Apply an action to multiple plugins and return a message with the results. - If only one extension is given, it is deferred to `manage()`. + If only one plugin is given, it is deferred to `manage()`. """ if len(plugs) == 1: msg, _ = self.manage(action, plugs[0]) diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py index b7cd2e20..c484ba7f 100644 --- a/modmail/utils/plugin_manager.py +++ b/modmail/utils/plugin_manager.py @@ -4,7 +4,7 @@ """ Helper utililites for managing plugins. -TODO: Expand file to download extensions from github and gitlab from a list that is passed. +TODO: Expand file to download plugins from github and gitlab from a list that is passed. """ @@ -34,7 +34,7 @@ def unqualify(name: str) -> str: def walk_plugins() -> Iterator[str]: - """Yield extension names from the modmail.plugins subpackage.""" + """Yield plugin names from the modmail.plugins subpackage.""" for path in BASE_PATH.glob("*/*.py"): # calculate the module name, if it were to have a name from the path relative_path = path.relative_to(BASE_PATH) @@ -47,7 +47,7 @@ def walk_plugins() -> Iterator[str]: spec.loader.exec_module(imported) if not inspect.isfunction(getattr(imported, "setup", None)): - # If it lacks a setup function, it's not an extension. + # If it lacks a setup function, it's not an plugin. continue if (ext_metadata := getattr(imported, "EXT_METADATA", None)) is not None: From 0ac701fd1aa6d352273e8d60f7547208a980fb32 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 23:11:04 -0400 Subject: [PATCH 023/169] fix: ensure modmail.plugins is a module --- modmail/plugins/.gitignore | 10 ++++++++-- modmail/plugins/__init__.py | 0 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 modmail/plugins/__init__.py diff --git a/modmail/plugins/.gitignore b/modmail/plugins/.gitignore index 43ec4561..e4b354c7 100644 --- a/modmail/plugins/.gitignore +++ b/modmail/plugins/.gitignore @@ -1,5 +1,11 @@ +# start by ignoring all files * -local/* + +# don't ignore this file !.gitignore -!local + +# ignore the local folder, but not the readme !local/readme.md + +# ensure this file is uploaded so `plugins` is considered a module +!__init__.py diff --git a/modmail/plugins/__init__.py b/modmail/plugins/__init__.py new file mode 100644 index 00000000..e69de29b From 3360807308621ff63e90c87b3e79e274dc6546ea Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 23:44:13 -0400 Subject: [PATCH 024/169] chore: don't use private variables --- modmail/__init__.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index 5eb488f4..5e90cd6f 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -2,13 +2,12 @@ import logging.handlers import typing from pathlib import Path -from typing import TYPE_CHECKING import coloredlogs from modmail.log import ModmailLogger -if TYPE_CHECKING: +if typing.TYPE_CHECKING: from modmail.bot import ModmailBot logging.TRACE = 5 @@ -16,9 +15,11 @@ logging.addLevelName(logging.TRACE, "TRACE") logging.addLevelName(logging.NOTICE, "NOTICE") -LOG_LEVEL = 5 -_FMT = "%(asctime)s %(levelname)10s %(name)15s - [%(lineno)5d]: %(message)s" -_DATEFMT = "%Y/%m/%d %H:%M:%S" +# this logging level is low because if it is not low, +# child logger will not be able to be at a lower level for debugging +ROOT_LOG_LEVEL = 5 +FMT = "%(asctime)s %(levelname)10s %(name)15s - [%(lineno)5d]: %(message)s" +DATEFMT = "%Y/%m/%d %H:%M:%S" logging.setLoggerClass(ModmailLogger) @@ -36,22 +37,22 @@ file_handler.setFormatter( logging.Formatter( - fmt=_FMT, - datefmt=_DATEFMT, + fmt=FMT, + datefmt=DATEFMT, ) ) file_handler.setLevel(logging.TRACE) # configure trace color -_LEVEL_STYLES = dict(coloredlogs.DEFAULT_LEVEL_STYLES) -_LEVEL_STYLES["trace"] = _LEVEL_STYLES["spam"] +LEVEL_STYLES = dict(coloredlogs.DEFAULT_LEVEL_STYLES) +LEVEL_STYLES["trace"] = LEVEL_STYLES["spam"] -coloredlogs.install(level=logging.TRACE, fmt=_FMT, datefmt=_DATEFMT, level_styles=_LEVEL_STYLES) +coloredlogs.install(level=logging.TRACE, fmt=FMT, datefmt=DATEFMT, level_styles=LEVEL_STYLES) # Create root logger root: ModmailLogger = logging.getLogger() -root.setLevel(LOG_LEVEL) +root.setLevel(ROOT_LOG_LEVEL) root.addHandler(file_handler) # Silence irrelevant loggers From caae0688f7cccd460166552b3d184073ec65ab13 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 5 Aug 2021 23:45:54 -0400 Subject: [PATCH 025/169] chore: don't create two bots --- modmail/__main__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modmail/__main__.py b/modmail/__main__.py index fc9592ad..e9769cfc 100644 --- a/modmail/__main__.py +++ b/modmail/__main__.py @@ -16,10 +16,9 @@ def main() -> None: """Run the bot.""" bot = ModmailBot() - log.notice("running bot") - bot.instance = ModmailBot() - bot.instance.load_extensions() - bot.instance.run(bot.config.bot.token) + bot.load_extensions() + log.notice("Running the bot.") + bot.run(bot.config.bot.token) if __name__ == "__main__": From 7716d2c35906f4ac13fe74cec35e596001c54737 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 6 Aug 2021 00:53:45 -0400 Subject: [PATCH 026/169] feat: add custom extension and plugin cog class --- modmail/plugin_helpers.py | 18 ++++++++++++++++++ modmail/plugins/.gitignore | 11 ++++++----- modmail/plugins/local/README.md | 12 ++++++++++++ modmail/plugins/local/readme.md | 1 - modmail/utils/cogs.py | 19 ++++++++++++++++++- 5 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 modmail/plugin_helpers.py create mode 100644 modmail/plugins/local/README.md delete mode 100644 modmail/plugins/local/readme.md diff --git a/modmail/plugin_helpers.py b/modmail/plugin_helpers.py new file mode 100644 index 00000000..2997b280 --- /dev/null +++ b/modmail/plugin_helpers.py @@ -0,0 +1,18 @@ +from modmail.utils.cogs import ModmailCog + +__all__ = ["PluginCog"] + + +class PluginCog(ModmailCog): + """ + The base class that all cogs must inherit from. + + A cog is a collection of commands, listeners, and optional state to + help group commands together. More information on them can be found on + the :ref:`ext_commands_cogs` page. + + When inheriting from this class, the options shown in :class:`CogMeta` + are equally valid here. + """ + + pass diff --git a/modmail/plugins/.gitignore b/modmail/plugins/.gitignore index e4b354c7..4be6c40b 100644 --- a/modmail/plugins/.gitignore +++ b/modmail/plugins/.gitignore @@ -1,11 +1,12 @@ # start by ignoring all files -* - +/* # don't ignore this file -!.gitignore +!/.gitignore # ignore the local folder, but not the readme -!local/readme.md +local/** +!local/ +!local/README.md # ensure this file is uploaded so `plugins` is considered a module -!__init__.py +!/__init__.py diff --git a/modmail/plugins/local/README.md b/modmail/plugins/local/README.md new file mode 100644 index 00000000..817f6aba --- /dev/null +++ b/modmail/plugins/local/README.md @@ -0,0 +1,12 @@ +# Plugins + +This folder is where local plugins can be put for developing. + +Plugins should be like normal discord cogs, but should subclass `PluginCog` from `modmail.plugin_helpers` + +```py +from modmail.plugin_helpers import PluginCog + +class MyPlugin(PluginCog): + pass +``` diff --git a/modmail/plugins/local/readme.md b/modmail/plugins/local/readme.md deleted file mode 100644 index acd4e373..00000000 --- a/modmail/plugins/local/readme.md +++ /dev/null @@ -1 +0,0 @@ -This folder is where local plugins can be put for developing. diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index 8fb04005..abfe3b45 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from enum import IntEnum +from discord.ext import commands + class BotModes(IntEnum): """ @@ -35,6 +37,21 @@ class ExtMetadata: def calc_mode(metadata: ExtMetadata) -> int: """Calculate the combination of different variables and return the binary combination.""" mode = int(getattr(metadata, "production", False)) - mode = mode + int(getattr(metadata, "develop", False) << 1) or 0 + mode += int(getattr(metadata, "develop", False) << 1) or 0 mode = mode + (int(getattr(metadata, "plugin_dev", False)) << 2) return mode + + +class ModmailCog(commands.Cog): + """ + The base class that all cogs must inherit from. + + A cog is a collection of commands, listeners, and optional state to + help group commands together. More information on them can be found on + the :ref:`ext_commands_cogs` page. + + When inheriting from this class, the options shown in :class:`CogMeta` + are equally valid here. + """ + + pass From 18850bb990f5718c45e2992d93a2f7764ef11d5e Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 6 Aug 2021 00:58:04 -0400 Subject: [PATCH 027/169] Minor review cleanup - use count() instead of split() - use _ instead of _nul - reorder if-else chain - use join instead of a for loop --- modmail/exts/utils/extensions.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/utils/extensions.py index cf2318d3..a09cde27 100644 --- a/modmail/exts/utils/extensions.py +++ b/modmail/exts/utils/extensions.py @@ -22,7 +22,7 @@ UNLOAD_BLACKLIST = { __name__, } -BASE_PATH_LEN = len(exts.__name__.split(".")) +BASE_PATH_LEN = exts.__name__.count(".") + 1 EXT_METADATA = ExtMetadata(develop=True) @@ -51,12 +51,13 @@ async def convert(self, ctx: Context, argument: str) -> str: argument = argument.lower() extensions = [] - for ext, _nul in EXTENSIONS: + for ext, _ in EXTENSIONS: extensions.append(ext) if argument in extensions: return argument - elif (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS: + + if (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS: return qualified_arg matches = [] @@ -64,17 +65,16 @@ async def convert(self, ctx: Context, argument: str) -> str: if argument == unqualify(ext): matches.append(ext) - if len(matches) > 1: - matches.sort() - names = "\n".join(matches) + if not matches: + raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") + elif len(matches) > 1: + names = "\n".join(sorted(matches)) raise commands.BadArgument( f":x: `{argument}` is an ambiguous extension name. " f"Please use one of the following fully-qualified names.```\n{names}```" ) - elif matches: - return matches[0] else: - raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") + return matches[0] class Extensions(commands.Cog): @@ -179,10 +179,7 @@ async def list_command(self, ctx: Context) -> None: log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") # since we currently don't have a paginator. - output = "" - for line in lines: - output += line - await ctx.send(output) + await ctx.send("".join(lines)) def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" From ca35405a905290bde9d15ec0ffdf76e3e7d6ce02 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 6 Aug 2021 02:29:09 -0400 Subject: [PATCH 028/169] chore: switch to using a dict for EXTENSIONS switch to using a dict for EXTENSIONS use a BitwiseAutoEnum for bot modes --- modmail/bot.py | 4 +--- modmail/exts/meta.py | 3 +++ modmail/exts/utils/extensions.py | 27 ++++++++++----------------- modmail/exts/utils/plugin_manager.py | 2 +- modmail/utils/cogs.py | 19 +++++++++++++------ modmail/utils/extensions.py | 3 ++- modmail/utils/plugin_manager.py | 4 ++-- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 341203ba..0aacdb8d 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -54,9 +54,7 @@ def load_extensions(self) -> None: # Must be done here to avoid a circular import. from modmail.utils.extensions import EXTENSIONS - extensions = set(EXTENSIONS) # Create a mutable copy. - - for extension, should_load in extensions: + for extension, should_load in EXTENSIONS.items(): if should_load: self.load_extension(extension) diff --git a/modmail/exts/meta.py b/modmail/exts/meta.py index cec8ea68..a972c7fb 100644 --- a/modmail/exts/meta.py +++ b/modmail/exts/meta.py @@ -5,9 +5,12 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger +from modmail.utils.cogs import BotModes log: ModmailLogger = logging.getLogger(__name__) +print(BotModes.plugin_dev) + class Meta(commands.Cog): """Meta commands to get info about the bot itself.""" diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/utils/extensions.py index a09cde27..faf31bca 100644 --- a/modmail/exts/utils/extensions.py +++ b/modmail/exts/utils/extensions.py @@ -50,18 +50,15 @@ async def convert(self, ctx: Context, argument: str) -> str: return argument argument = argument.lower() - extensions = [] - for ext, _ in EXTENSIONS: - extensions.append(ext) - if argument in extensions: + if argument in EXTENSIONS.keys(): return argument - if (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS: + if (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS.keys(): return qualified_arg matches = [] - for ext in extensions: + for ext in EXTENSIONS: if argument == unqualify(ext): matches.append(ext) @@ -77,7 +74,7 @@ async def convert(self, ctx: Context, argument: str) -> str: return matches[0] -class Extensions(commands.Cog): +class ExtensionManager(commands.Cog): """Extension management commands.""" def __init__(self, bot: ModmailBot): @@ -100,7 +97,7 @@ async def load_command(self, ctx: Context, *extensions: Extension) -> None: return if "*" in extensions or "**" in extensions: - extensions = set(EXTENSIONS) - set(self.bot.extensions.keys()) + extensions = EXTENSIONS.keys() - set(self.bot.extensions.keys()) msg = self.batch_manage(Action.LOAD, *extensions) await ctx.send(msg) @@ -143,9 +140,7 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: return if "**" in extensions: - extensions = [] - for ext, _nul in EXTENSIONS: - extensions.append(ext) + extensions = EXTENSIONS.keys() elif "*" in extensions: extensions = set(self.bot.extensions.keys()) | set(extensions) extensions.remove("*") @@ -184,10 +179,8 @@ async def list_command(self, ctx: Context) -> None: def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" categories = {} - extensions = [] - for ext, _nul in EXTENSIONS: - extensions.append(ext) - for ext in extensions: + + for ext in EXTENSIONS.keys(): if ext in self.bot.extensions: status = ":green_circle:" else: @@ -216,7 +209,7 @@ def batch_manage(self, action: Action, *extensions: str) -> str: verb = action.name.lower() failures = {} - for extension in extensions: + for extension in sorted(extensions): _, error = self.manage(action, extension) if error: failures[extension] = error @@ -276,4 +269,4 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: def setup(bot: ModmailBot) -> None: """Load the Extensions cog.""" - bot.add_cog(Extensions(bot)) + bot.add_cog(ExtensionManager(bot)) diff --git a/modmail/exts/utils/plugin_manager.py b/modmail/exts/utils/plugin_manager.py index 31380702..d69f0eb3 100644 --- a/modmail/exts/utils/plugin_manager.py +++ b/modmail/exts/utils/plugin_manager.py @@ -81,7 +81,7 @@ class PluginManager(commands.Cog, name="Plugin Manager"): def __init__(self, bot: ModmailBot): self.bot = bot - @group(name="plugins", aliases=("plug", "plugs"), invoke_without_command=True) + @group(name="plugins", aliases=("plug", "plugs", "plugin"), invoke_without_command=True) async def plugins_group(self, ctx: Context) -> None: """Load, unload, reload, and list loaded plugins.""" await ctx.send_help(ctx.command) diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index abfe3b45..bb99d168 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -1,19 +1,26 @@ from dataclasses import dataclass -from enum import IntEnum +from enum import IntEnum, auto from discord.ext import commands -class BotModes(IntEnum): +class BitwiseAutoEnum(IntEnum): + """Enum class which generates binary value for each item.""" + + def _generate_next_value_(name, start, count, last_values) -> int: # noqa: ANN001 + return 1 << count + + +class BotModes(BitwiseAutoEnum): """ Valid modes for the bot. - These values affect logging levels, which logs are loaded, and so forth. + These values affect logging levels, which extensions are loaded, and so forth. """ - production = int("1", 2) - develop = int("10", 2) - plugin_dev = int("100", 2) + production = auto() + develop = auto() + plugin_dev = auto() BOT_MODES = BotModes diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 9579914a..fe9c03d9 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -56,7 +56,8 @@ def on_error(name: str) -> NoReturn: log.notice(f"Cog {module.name!r} is missing an EXT_METADATA variable. Assuming its a normal cog.") + # Presume Production Mode yield (module.name, True) -EXTENSIONS = frozenset(walk_extensions()) +EXTENSIONS = dict(walk_extensions()) diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py index c484ba7f..678af91b 100644 --- a/modmail/utils/plugin_manager.py +++ b/modmail/utils/plugin_manager.py @@ -52,7 +52,7 @@ def walk_plugins() -> Iterator[str]: if (ext_metadata := getattr(imported, "EXT_METADATA", None)) is not None: # check if this plugin is dev only or plugin dev only - load_cog = bool(calc_mode(ext_metadata) & BOT_MODE) + load_cog = calc_mode(ext_metadata & BOT_MODE) log.trace(f"Load plugin {imported.__name__!r}?: {load_cog}") yield imported.__name__, load_cog continue @@ -64,4 +64,4 @@ def walk_plugins() -> Iterator[str]: yield (imported.__name__, True) -PLUGINS = frozenset(walk_plugins()) +PLUGINS = dict(walk_plugins()) From 705373398dd695b73c56dd405b2befe221889f9a Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 6 Aug 2021 02:50:26 -0400 Subject: [PATCH 029/169] chore: use logging levels for logging level --- modmail/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index 5e90cd6f..fc6cd087 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -15,9 +15,9 @@ logging.addLevelName(logging.TRACE, "TRACE") logging.addLevelName(logging.NOTICE, "NOTICE") -# this logging level is low because if it is not low, -# child logger will not be able to be at a lower level for debugging -ROOT_LOG_LEVEL = 5 +# this logging level is set to logging.TRACE because if it is not set to the lowest level, +# the child level will be limited to the lowest level this is set to. +ROOT_LOG_LEVEL = logging.TRACE FMT = "%(asctime)s %(levelname)10s %(name)15s - [%(lineno)5d]: %(message)s" DATEFMT = "%Y/%m/%d %H:%M:%S" From cdd727bf6b66f02a11fd9b69fd9eb3ec38854588 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 6 Aug 2021 03:02:42 -0400 Subject: [PATCH 030/169] chore: ignore N805 --- modmail/utils/cogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index bb99d168..31fc78c6 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -7,7 +7,7 @@ class BitwiseAutoEnum(IntEnum): """Enum class which generates binary value for each item.""" - def _generate_next_value_(name, start, count, last_values) -> int: # noqa: ANN001 + def _generate_next_value_(name, start, count, last_values) -> int: # noqa: ANN001 N805 return 1 << count From da8acef6309a44187824b91c8e86e4f72c3dda88 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Fri, 6 Aug 2021 13:58:46 +0530 Subject: [PATCH 031/169] Don't use sets for working with plugs/ext --- modmail/__init__.py | 2 +- modmail/exts/meta.py | 2 +- modmail/exts/utils/extensions.py | 8 ++++---- modmail/exts/utils/plugin_manager.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index fc6cd087..d948eaac 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -24,7 +24,7 @@ logging.setLoggerClass(ModmailLogger) # Set up file logging -log_file = Path("./logs/bot.log") +log_file = Path("logs", "bot.log") log_file.parent.mkdir(parents=True, exist_ok=True) # file handler diff --git a/modmail/exts/meta.py b/modmail/exts/meta.py index a972c7fb..8853f6f8 100644 --- a/modmail/exts/meta.py +++ b/modmail/exts/meta.py @@ -26,7 +26,7 @@ async def ping(self, ctx: Context) -> None: @commands.command(name="uptime") async def uptime(self, ctx: commands.Context) -> None: """Get the current uptime of the bot.""" - timestamp = self.bot.start_time.format("X").split(".")[0] + timestamp = round(float(self.bot.start_time.format("X"))) await ctx.send(f"Start time: ") @commands.command(name="prefix") diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/utils/extensions.py index faf31bca..d593ff91 100644 --- a/modmail/exts/utils/extensions.py +++ b/modmail/exts/utils/extensions.py @@ -97,7 +97,7 @@ async def load_command(self, ctx: Context, *extensions: Extension) -> None: return if "*" in extensions or "**" in extensions: - extensions = EXTENSIONS.keys() - set(self.bot.extensions.keys()) + extensions = [ext for ext in EXTENSIONS if ext not in self.bot.extensions.keys()] msg = self.batch_manage(Action.LOAD, *extensions) await ctx.send(msg) @@ -113,13 +113,13 @@ async def unload_command(self, ctx: Context, *extensions: Extension) -> None: await ctx.send_help(ctx.command) return - blacklisted = "\n".join(UNLOAD_BLACKLIST & set(extensions)) + blacklisted = "\n".join([ext for ext in UNLOAD_BLACKLIST if ext in extensions]) if blacklisted: msg = f":x: The following extension(s) may not be unloaded:```\n{blacklisted}```" else: if "*" in extensions or "**" in extensions: - extensions = set(self.bot.extensions.keys()) - UNLOAD_BLACKLIST + extensions = [ext for ext in self.bot.extensions.keys() if ext not in UNLOAD_BLACKLIST] msg = self.batch_manage(Action.UNLOAD, *extensions) @@ -142,7 +142,7 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: if "**" in extensions: extensions = EXTENSIONS.keys() elif "*" in extensions: - extensions = set(self.bot.extensions.keys()) | set(extensions) + extensions = (self.bot.extensions.keys()).extend(extensions) extensions.remove("*") msg = self.batch_manage(Action.RELOAD, *extensions) diff --git a/modmail/exts/utils/plugin_manager.py b/modmail/exts/utils/plugin_manager.py index d69f0eb3..729b1a98 100644 --- a/modmail/exts/utils/plugin_manager.py +++ b/modmail/exts/utils/plugin_manager.py @@ -20,7 +20,7 @@ log: ModmailLogger = logging.getLogger(__name__) BASE_PATH = Path(plugins.__file__).parent -BASE_PATH_LEN = len(plugins.__name__.split(".")) +BASE_PATH_LEN = plugins.__name__.count(".") + 1 EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) @@ -98,7 +98,7 @@ async def load_command(self, ctx: Context, *plugs: Plugin) -> None: return if "*" in plugs or "**" in plugs: - plugs = set(PLUGINS) - set(self.bot.extensions.keys()) + plugs = [plug for plug in PLUGINS if plug not in self.bot.extensions.keys()] msg = self.batch_manage(Action.LOAD, *plugs) await ctx.send(msg) @@ -115,7 +115,7 @@ async def unload_command(self, ctx: Context, *plugs: Plugin) -> None: return if "*" in plugs or "**" in plugs: - plugs = set(self.bot.extensions.keys()) + plugs = self.bot.extensions.keys() msg = self.batch_manage(Action.UNLOAD, *plugs) @@ -140,7 +140,7 @@ async def reload_command(self, ctx: Context, *plugs: Plugin) -> None: for plug, _nul in PLUGINS: plugs.append(plug) elif "*" in plugs: - plugs = set(self.bot.extensions.keys()) | set(plugs) + plugs = (self.bot.extensions.keys()).extend(plugs) plugs.remove("*") msg = self.batch_manage(Action.RELOAD, *plugs) From a6c80cefe66c4c8681db5b09beff046d8a27f331 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Sat, 7 Aug 2021 05:29:14 +0530 Subject: [PATCH 032/169] Use baseclass for plugins and cogs, together called extensions --- .../exts/{utils => extensions}/__init__.py | 0 .../_base_class.py} | 97 ++++-- modmail/exts/extensions/_cogs.py | 18 ++ modmail/exts/extensions/_plugins.py | 18 ++ modmail/exts/utils/plugin_manager.py | 276 ------------------ 5 files changed, 104 insertions(+), 305 deletions(-) rename modmail/exts/{utils => extensions}/__init__.py (100%) rename modmail/exts/{utils/extensions.py => extensions/_base_class.py} (77%) create mode 100644 modmail/exts/extensions/_cogs.py create mode 100644 modmail/exts/extensions/_plugins.py delete mode 100644 modmail/exts/utils/plugin_manager.py diff --git a/modmail/exts/utils/__init__.py b/modmail/exts/extensions/__init__.py similarity index 100% rename from modmail/exts/utils/__init__.py rename to modmail/exts/extensions/__init__.py diff --git a/modmail/exts/utils/extensions.py b/modmail/exts/extensions/_base_class.py similarity index 77% rename from modmail/exts/utils/extensions.py rename to modmail/exts/extensions/_base_class.py index d593ff91..03198945 100644 --- a/modmail/exts/utils/extensions.py +++ b/modmail/exts/extensions/_base_class.py @@ -8,23 +8,20 @@ from discord import Colour, Embed from discord.ext import commands -from discord.ext.commands import Context, group +from discord.ext.commands import Context, Group, command from modmail import exts from modmail.bot import ModmailBot from modmail.log import ModmailLogger from modmail.utils.cogs import ExtMetadata from modmail.utils.extensions import EXTENSIONS, unqualify +from modmail.utils.plugin_manager import PLUGINS log: ModmailLogger = logging.getLogger(__name__) - -UNLOAD_BLACKLIST = { - __name__, -} BASE_PATH_LEN = exts.__name__.count(".") + 1 -EXT_METADATA = ExtMetadata(develop=True) +EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) class Action(Enum): @@ -45,20 +42,25 @@ class Extension(commands.Converter): async def convert(self, ctx: Context, argument: str) -> str: """Fully qualify the name of an extension and ensure it exists.""" + if (ctx.command.name).lower() == "cog": + extensions_all = EXTENSIONS + elif (ctx.command.name).lower() == "plugin": + extensions_all = PLUGINS + # Special values to reload all extensions if argument == "*" or argument == "**": return argument argument = argument.lower() - if argument in EXTENSIONS.keys(): + if argument in extensions_all.keys(): return argument - if (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS.keys(): + if (qualified_arg := f"{exts.__name__}.{argument}") in extensions_all.keys(): return qualified_arg matches = [] - for ext in EXTENSIONS: + for ext in extensions_all: if argument == unqualify(ext): matches.append(ext) @@ -74,52 +76,91 @@ async def convert(self, ctx: Context, argument: str) -> str: return matches[0] +def custom_group() -> t.Callable: + """ + Custom command `group` decorator. + + Reads the `name` and `alias` attributes from the decorator and passes it on to the group. + """ + + def decorator(function: t.Callable): + @functools.wraps(function) + def wrapper(self: t.Any, *args): + args.setdefault("cls", Group) + return command( + name=self.extension_type, + aliases=self.aliases, + help=f"Load, unload, reload, and list loaded {self.extension_type}.", + **args, + ) + + return wrapper + + return decorator + + class ExtensionManager(commands.Cog): - """Extension management commands.""" + """Extension management base class.""" - def __init__(self, bot: ModmailBot): + def __init__(self, bot: ModmailBot, extension_type: str, aliases: t.Optional[t.Tuple[str]] = None): self.bot = bot + self.extension_type = extension_type.lower() + self.aliases = aliases or () + + _all_mapping = {"cog": EXTENSIONS.copy(), "plugin": PLUGINS.copy()} + self.all_extensions = _all_mapping.get(extension_type) - @group(name="extensions", aliases=("ext", "exts", "c", "cogs"), invoke_without_command=True) + if not self.all_extensions: + raise ValueError( + f"Looks like you have given an incorrect {extension_type}, " + "valid options are: {', '.join(_all_mapping.keys())}" + ) + + async def get_black_listed_extensions() -> list: + """Returns a list of all blacklisted extensions.""" + raise NotImplementedError() + + @custom_group(invoke_without_command=True) async def extensions_group(self, ctx: Context) -> None: """Load, unload, reload, and list loaded extensions.""" await ctx.send_help(ctx.command) @extensions_group.command(name="load", aliases=("l",)) async def load_command(self, ctx: Context, *extensions: Extension) -> None: - r""" + """ Load extensions given their fully qualified or unqualified names. If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. - """ # noqa: W605 + """ if not extensions: await ctx.send_help(ctx.command) return if "*" in extensions or "**" in extensions: - extensions = [ext for ext in EXTENSIONS if ext not in self.bot.extensions.keys()] + extensions = [ext for ext in self.all_extensions if ext not in self.bot.extensions.keys()] msg = self.batch_manage(Action.LOAD, *extensions) await ctx.send(msg) @extensions_group.command(name="unload", aliases=("ul",)) async def unload_command(self, ctx: Context, *extensions: Extension) -> None: - r""" + """ Unload currently loaded extensions given their fully qualified or unqualified names. If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. - """ # noqa: W605 + """ if not extensions: await ctx.send_help(ctx.command) return - blacklisted = "\n".join([ext for ext in UNLOAD_BLACKLIST if ext in extensions]) + unload_blacklist = await self.get_black_listed_extensions() + blacklisted = "\n".join([ext for ext in unload_blacklist if ext in extensions]) if blacklisted: msg = f":x: The following extension(s) may not be unloaded:```\n{blacklisted}```" else: if "*" in extensions or "**" in extensions: - extensions = [ext for ext in self.bot.extensions.keys() if ext not in UNLOAD_BLACKLIST] + extensions = [ext for ext in self.bot.extensions.keys() if ext not in unload_blacklist] msg = self.batch_manage(Action.UNLOAD, *extensions) @@ -127,20 +168,20 @@ async def unload_command(self, ctx: Context, *extensions: Extension) -> None: @extensions_group.command(name="reload", aliases=("r",)) async def reload_command(self, ctx: Context, *extensions: Extension) -> None: - r""" + """ Reload extensions given their fully qualified or unqualified names. If an extension fails to be reloaded, it will be rolled-back to the prior working state. If '\*' is given as the name, all currently loaded extensions will be reloaded. If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. - """ # noqa: W605 + """ if not extensions: await ctx.send_help(ctx.command) return if "**" in extensions: - extensions = EXTENSIONS.keys() + extensions = self.all_extensions.keys() elif "*" in extensions: extensions = (self.bot.extensions.keys()).extend(extensions) extensions.remove("*") @@ -171,7 +212,10 @@ async def list_command(self, ctx: Context) -> None: extensions = "\n".join(sorted(extensions)) lines.append(f"**{category}**\n{extensions}\n") - log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") + log.debug( + f"{ctx.author} requested a list of all {self.extension_type.lower()}s. " + "Returning a paginated list." + ) # since we currently don't have a paginator. await ctx.send("".join(lines)) @@ -180,7 +224,7 @@ def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" categories = {} - for ext in EXTENSIONS.keys(): + for ext in self.all_extensions.keys(): if ext in self.bot.extensions: status = ":green_circle:" else: @@ -265,8 +309,3 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: if isinstance(error, commands.BadArgument): await ctx.send(str(error)) error.handled = True - - -def setup(bot: ModmailBot) -> None: - """Load the Extensions cog.""" - bot.add_cog(ExtensionManager(bot)) diff --git a/modmail/exts/extensions/_cogs.py b/modmail/exts/extensions/_cogs.py new file mode 100644 index 00000000..ae41be02 --- /dev/null +++ b/modmail/exts/extensions/_cogs.py @@ -0,0 +1,18 @@ +from modmail import ModmailBot +from modmail.exts.extensions._base_class import ExtensionManager + + +class CogsManager(ExtensionManager): + """Cogs management commands.""" + + def __init__(self, bot: ModmailBot) -> None: + self.bot = bot + + _extension_type = "cog" + _aliases = ("ext", "exts", "c", "cogs") + ExtensionManager.__init__(self, bot, _extension_type, _aliases) + + +def setup(bot: ModmailBot) -> None: + """Load the Cogs manager cog.""" + bot.add_cog(CogsManager(bot)) diff --git a/modmail/exts/extensions/_plugins.py b/modmail/exts/extensions/_plugins.py new file mode 100644 index 00000000..0f22a9ab --- /dev/null +++ b/modmail/exts/extensions/_plugins.py @@ -0,0 +1,18 @@ +from modmail import ModmailBot +from modmail.exts.extensions._base_class import ExtensionManager + + +class PluginsManager(ExtensionManager): + """Plugin management commands.""" + + def __init__(self, bot: ModmailBot) -> None: + self.bot = bot + + _extension_type = "plugin" + _aliases = ("plug", "plugs", "plugins") + ExtensionManager.__init__(self, bot, _extension_type, _aliases) + + +def setup(bot: ModmailBot) -> None: + """Load the Plugins manager cog.""" + bot.add_cog(PluginsManager(bot)) diff --git a/modmail/exts/utils/plugin_manager.py b/modmail/exts/utils/plugin_manager.py deleted file mode 100644 index 729b1a98..00000000 --- a/modmail/exts/utils/plugin_manager.py +++ /dev/null @@ -1,276 +0,0 @@ -# original source: -# https://github.com/python-discord/bot/blob/a8869b4d60512b173871c886321b261cbc4acca9/bot/exts/utils/extensions.py # noqa: E501 -# MIT License 2021 Python Discord -import functools -import logging -import typing as t -from enum import Enum -from pathlib import Path - -from discord import Colour, Embed -from discord.ext import commands -from discord.ext.commands import Context, group - -from modmail import plugins -from modmail.bot import ModmailBot -from modmail.log import ModmailLogger -from modmail.utils.cogs import ExtMetadata -from modmail.utils.plugin_manager import PLUGINS, unqualify - -log: ModmailLogger = logging.getLogger(__name__) - -BASE_PATH = Path(plugins.__file__).parent -BASE_PATH_LEN = plugins.__name__.count(".") + 1 - -EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) - - -class Action(Enum): - """Represents an action to perform on an plugin.""" - - # Need to be partial otherwise they are considered to be function definitions. - LOAD = functools.partial(ModmailBot.load_extension) - UNLOAD = functools.partial(ModmailBot.unload_extension) - RELOAD = functools.partial(ModmailBot.reload_extension) - - -class Plugin(commands.Converter): - """ - Fully qualify the name of an plugin and ensure it exists. - - The * and ** values bypass this when used with the reload command. - """ - - async def convert(self, ctx: Context, argument: str) -> str: - """Fully qualify the name of an plugin and ensure it exists.""" - # Special values to reload all plugins - if argument == "*" or argument == "**": - return argument - - argument = argument.lower() - plugs = [] - for ext, _nul in PLUGINS: - plugs.append(ext) - - if argument in plugs: - return argument - elif (qualified_arg := f"{plugins.__name__}.{argument}") in PLUGINS: - return qualified_arg - - matches = [] - for ext in plugs: - if argument == unqualify(ext): - matches.append(ext) - - if len(matches) > 1: - matches.sort() - names = "\n".join(matches) - raise commands.BadArgument( - f":x: `{argument}` is an ambiguous plugin name. " - f"Please use one of the following fully-qualified names.```\n{names}```" - ) - elif matches: - return matches[0] - else: - raise commands.BadArgument(f":x: Could not find the plugin `{argument}`.") - - -class PluginManager(commands.Cog, name="Plugin Manager"): - """Plugin management commands.""" - - def __init__(self, bot: ModmailBot): - self.bot = bot - - @group(name="plugins", aliases=("plug", "plugs", "plugin"), invoke_without_command=True) - async def plugins_group(self, ctx: Context) -> None: - """Load, unload, reload, and list loaded plugins.""" - await ctx.send_help(ctx.command) - - @plugins_group.command(name="load", aliases=("l",)) - async def load_command(self, ctx: Context, *plugs: Plugin) -> None: - r""" - Load plugins given their fully qualified or unqualified names. - - If '\*' or '\*\*' is given as the name, all unloaded plugins will be loaded. - """ # noqa: W605 - if not plugs: - await ctx.send_help(ctx.command) - return - - if "*" in plugs or "**" in plugs: - plugs = [plug for plug in PLUGINS if plug not in self.bot.extensions.keys()] - - msg = self.batch_manage(Action.LOAD, *plugs) - await ctx.send(msg) - - @plugins_group.command(name="unload", aliases=("ul",)) - async def unload_command(self, ctx: Context, *plugs: Plugin) -> None: - r""" - Unload currently loaded plugins given their fully qualified or unqualified names. - - If '\*' or '\*\*' is given as the name, all loaded plugins will be unloaded. - """ # noqa: W605 - if not plugs: - await ctx.send_help(ctx.command) - return - - if "*" in plugs or "**" in plugs: - plugs = self.bot.extensions.keys() - - msg = self.batch_manage(Action.UNLOAD, *plugs) - - await ctx.send(msg) - - @plugins_group.command(name="reload", aliases=("r",)) - async def reload_command(self, ctx: Context, *plugs: Plugin) -> None: - r""" - Reload plugins given their fully qualified or unqualified names. - - If an plugin fails to be reloaded, it will be rolled-back to the prior working state. - - If '\*' is given as the name, all currently loaded plugins will be reloaded. - If '\*\*' is given as the name, all plugins, including unloaded ones, will be reloaded. - """ # noqa: W605 - if not plugs: - await ctx.send_help(ctx.command) - return - - if "**" in plugs: - plugs = [] - for plug, _nul in PLUGINS: - plugs.append(plug) - elif "*" in plugs: - plugs = (self.bot.extensions.keys()).extend(plugs) - plugs.remove("*") - - msg = self.batch_manage(Action.RELOAD, *plugs) - - await ctx.send(msg) - - @plugins_group.command(name="list", aliases=("all",)) - async def list_command(self, ctx: Context) -> None: - """ - Get a list of all plugins, including their loaded status. - - Grey indicates that the plugins is unloaded. - Green indicates that the plugins is currently loaded. - """ - embed = Embed(colour=Colour.blurple()) - embed.set_author( - name="Plugins List", - ) - - lines = [] - categories = self.group_plugin_statuses() - for category, plugs in sorted(categories.items()): - # Treat each category as a single line by concatenating everything. - # This ensures the paginator will not cut off a page in the middle of a category. - category = category.replace("_", " ").title() - plugs = "\n".join(sorted(plugs)) - lines.append(f"**{category}**\n{plugs}\n") - - log.debug(f"{ctx.author} requested a list of all cogs. Returning a list.") - # await Paginator.paginate(lines, ctx, embed, scale_to_size=700, empty=False) - - # since we currently don't have a paginator. - output = "" - for line in lines: - output += line - await ctx.send(output) - - def group_plugin_statuses(self) -> t.Mapping[str, str]: - """Return a mapping of plugin names and statuses to their categories.""" - categories = {} - plugs = [] - for ext, _nul in PLUGINS: - plugs.append(ext) - for plug in plugs: - if plug in self.bot.extensions: - status = ":green_circle:" - else: - status = ":red_circle:" - - path = ext.split(".") - if len(path) > BASE_PATH_LEN + 1: - category = " - ".join(path[BASE_PATH_LEN:-1]) - else: - category = "uncategorised" - - categories.setdefault(category, []).append(f"{status} {path[-1]}") - - return categories - - def batch_manage(self, action: Action, *plugs: str) -> str: - """ - Apply an action to multiple plugins and return a message with the results. - - If only one plugin is given, it is deferred to `manage()`. - """ - if len(plugs) == 1: - msg, _ = self.manage(action, plugs[0]) - return msg - - verb = action.name.lower() - failures = {} - - for plug in plugs: - _, error = self.manage(action, plug) - if error: - failures[plug] = error - - emoji = ":x:" if failures else ":ok_hand:" - msg = f"{emoji} {len(plugs) - len(failures)} / {len(plugs)} plugins {verb}ed." - - if failures: - failures = "\n".join(f"{ext}\n {err}" for ext, err in failures.items()) - msg += f"\nFailures:```\n{failures}```" - - log.debug(f"Batch {verb}ed plugins.") - - return msg - - def manage(self, action: Action, plug: str) -> t.Tuple[str, t.Optional[str]]: - """Apply an action to an plugin and return the status message and any error message.""" - verb = action.name.lower() - error_msg = None - - try: - action.value(self.bot, plug) - except (commands.ExtensionAlreadyLoaded, commands.ExtensionNotLoaded): - if action is Action.RELOAD: - # When reloading, just load the plugin if it was not loaded. - log.debug("Treating {plug!r} as if it was not loaded.") - return self.manage(Action.LOAD, plug) - - msg = f":x: Plugin `{plug}` is already {verb}ed." - log.debug(msg[4:]) - except Exception as e: - if hasattr(e, "original"): - e = e.original - - log.exception(f"Plugin '{plug}' failed to {verb}.") - - error_msg = f"{e.__class__.__name__}: {e}" - msg = f":x: Failed to {verb} plugin `{plug}`:\n```\n{error_msg}```" - else: - msg = f":ok_hand: Plugin successfully {verb}ed: `{plug}`." - log.debug(msg[10:]) - - return msg, error_msg - - # This cannot be static (must have a __func__ attribute). - async def cog_check(self, ctx: Context) -> bool: - """Only allow bot owners to invoke the commands in this cog.""" - return await self.bot.is_owner(ctx.author) - - # This cannot be static (must have a __func__ attribute). - async def cog_command_error(self, ctx: Context, error: Exception) -> None: - """Handle BadArgument errors locally to prevent the help command from showing.""" - if isinstance(error, commands.BadArgument): - await ctx.send(str(error)) - error.handled = True - - -def setup(bot: ModmailBot) -> None: - """Load the Plugins manager cog.""" - bot.add_cog(PluginManager(bot)) From f7f1741fd80b03ecccfbe82abd45006584ac1ef6 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 6 Aug 2021 20:44:57 -0400 Subject: [PATCH 033/169] fix: lint and circular imports --- modmail/bot.py | 3 ++- modmail/exts/extensions/_base_class.py | 8 ++++---- modmail/exts/extensions/{_cogs.py => cogs.py} | 2 +- modmail/utils/extensions.py | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) rename modmail/exts/extensions/{_cogs.py => cogs.py} (93%) diff --git a/modmail/bot.py b/modmail/bot.py index 0aacdb8d..69dd1a88 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -52,8 +52,9 @@ async def close(self) -> None: def load_extensions(self) -> None: """Load all enabled extensions.""" # Must be done here to avoid a circular import. - from modmail.utils.extensions import EXTENSIONS + from modmail.utils.extensions import EXTENSIONS, walk_extensions + EXTENSIONS.update(walk_extensions()) for extension, should_load in EXTENSIONS.items(): if should_load: self.load_extension(extension) diff --git a/modmail/exts/extensions/_base_class.py b/modmail/exts/extensions/_base_class.py index 03198945..41c51727 100644 --- a/modmail/exts/extensions/_base_class.py +++ b/modmail/exts/extensions/_base_class.py @@ -131,7 +131,7 @@ async def load_command(self, ctx: Context, *extensions: Extension) -> None: Load extensions given their fully qualified or unqualified names. If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. - """ + """ # noqa: W605 if not extensions: await ctx.send_help(ctx.command) return @@ -148,7 +148,7 @@ async def unload_command(self, ctx: Context, *extensions: Extension) -> None: Unload currently loaded extensions given their fully qualified or unqualified names. If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. - """ + """ # noqa: W605 if not extensions: await ctx.send_help(ctx.command) return @@ -173,8 +173,8 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: If an extension fails to be reloaded, it will be rolled-back to the prior working state. - If '\*' is given as the name, all currently loaded extensions will be reloaded. - If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. + If '*' is given as the name, all currently loaded extensions will be reloaded. + If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. """ if not extensions: await ctx.send_help(ctx.command) diff --git a/modmail/exts/extensions/_cogs.py b/modmail/exts/extensions/cogs.py similarity index 93% rename from modmail/exts/extensions/_cogs.py rename to modmail/exts/extensions/cogs.py index ae41be02..ab3cd1ed 100644 --- a/modmail/exts/extensions/_cogs.py +++ b/modmail/exts/extensions/cogs.py @@ -1,4 +1,4 @@ -from modmail import ModmailBot +from modmail.bot import ModmailBot from modmail.exts.extensions._base_class import ExtensionManager diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index fe9c03d9..46329839 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -60,4 +60,4 @@ def on_error(name: str) -> NoReturn: yield (module.name, True) -EXTENSIONS = dict(walk_extensions()) +EXTENSIONS = dict() From 8f4c30435e1bcc278b87d738148731eed5431828 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 21:56:43 -0700 Subject: [PATCH 034/169] Move extensions code out of the extensions folder And adjust base path calculation for cleanliness --- .../{extensions/_base_class.py => core.py} | 32 ++++++++++++++----- modmail/exts/extensions/_plugins.py | 18 ----------- 2 files changed, 24 insertions(+), 26 deletions(-) rename modmail/exts/{extensions/_base_class.py => core.py} (93%) diff --git a/modmail/exts/extensions/_base_class.py b/modmail/exts/core.py similarity index 93% rename from modmail/exts/extensions/_base_class.py rename to modmail/exts/core.py index 41c51727..a4b2f147 100644 --- a/modmail/exts/extensions/_base_class.py +++ b/modmail/exts/core.py @@ -19,7 +19,7 @@ log: ModmailLogger = logging.getLogger(__name__) -BASE_PATH_LEN = exts.__name__.count(".") + 1 +BASE_PATH = exts.__name__.count(".") + 1 EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) @@ -83,9 +83,9 @@ def custom_group() -> t.Callable: Reads the `name` and `alias` attributes from the decorator and passes it on to the group. """ - def decorator(function: t.Callable): + def decorator(function: t.Callable) -> t.Callable: @functools.wraps(function) - def wrapper(self: t.Any, *args): + def wrapper(self: t.Any, *args) -> commands.Command: args.setdefault("cls", Group) return command( name=self.extension_type, @@ -230,13 +230,13 @@ def group_extension_statuses(self) -> t.Mapping[str, str]: else: status = ":red_circle:" - path = ext.split(".") - if len(path) > BASE_PATH_LEN + 1: - category = " - ".join(path[BASE_PATH_LEN:-1]) + root, name = ext.rsplit(".", 1) + if len(root) > len(BASE_PATH): + category = " - ".join(root[len(BASE_PATH) + 1 :].split(".")) else: - category = "uncategorised" + category = "uncategorized" - categories.setdefault(category, []).append(f"{status} {path[-1]}") + categories.setdefault(category, []).append(f"{status} {name}") return categories @@ -309,3 +309,19 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: if isinstance(error, commands.BadArgument): await ctx.send(str(error)) error.handled = True + + +class PluginsManager(ExtensionManager): + """Plugin management commands.""" + + def __init__(self, bot: ModmailBot) -> None: + self.bot = bot + + _extension_type = "plugin" + _aliases = ("plug", "plugs", "plugins") + ExtensionManager.__init__(self, bot, _extension_type, _aliases) + + +def setup(bot: ModmailBot) -> None: + """Load the Plugins manager cog.""" + bot.add_cog(PluginsManager(bot)) diff --git a/modmail/exts/extensions/_plugins.py b/modmail/exts/extensions/_plugins.py index 0f22a9ab..e69de29b 100644 --- a/modmail/exts/extensions/_plugins.py +++ b/modmail/exts/extensions/_plugins.py @@ -1,18 +0,0 @@ -from modmail import ModmailBot -from modmail.exts.extensions._base_class import ExtensionManager - - -class PluginsManager(ExtensionManager): - """Plugin management commands.""" - - def __init__(self, bot: ModmailBot) -> None: - self.bot = bot - - _extension_type = "plugin" - _aliases = ("plug", "plugs", "plugins") - ExtensionManager.__init__(self, bot, _extension_type, _aliases) - - -def setup(bot: ModmailBot) -> None: - """Load the Plugins manager cog.""" - bot.add_cog(PluginsManager(bot)) From eb129d36bc47df2f05728a17c357270b1533547a Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:00:26 -0700 Subject: [PATCH 035/169] Load extensions in sorted order --- modmail/exts/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index a4b2f147..185e7295 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -137,7 +137,7 @@ async def load_command(self, ctx: Context, *extensions: Extension) -> None: return if "*" in extensions or "**" in extensions: - extensions = [ext for ext in self.all_extensions if ext not in self.bot.extensions.keys()] + extensions = sorted(ext for ext in self.all_extensions if ext not in self.bot.extensions.keys()) msg = self.batch_manage(Action.LOAD, *extensions) await ctx.send(msg) From 043921f70a0e2fcbf04bfbcd71d7752395da1997 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:04:28 -0700 Subject: [PATCH 036/169] Rewrite how blacklisted extensions are handled in unload --- modmail/exts/core.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index 185e7295..214af854 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -153,18 +153,17 @@ async def unload_command(self, ctx: Context, *extensions: Extension) -> None: await ctx.send_help(ctx.command) return - unload_blacklist = await self.get_black_listed_extensions() - blacklisted = "\n".join([ext for ext in unload_blacklist if ext in extensions]) + blacklisted = [ext for ext in await self.get_black_listed_extensions() if ext in extensions] if blacklisted: - msg = f":x: The following extension(s) may not be unloaded:```\n{blacklisted}```" - else: - if "*" in extensions or "**" in extensions: - extensions = [ext for ext in self.bot.extensions.keys() if ext not in unload_blacklist] + bl_msg = "\n".join(blacklisted) + await ctx.send(f":x: The following extension(s) may not be unloaded:```\n{bl_msg}```") + return - msg = self.batch_manage(Action.UNLOAD, *extensions) + if "*" in extensions or "**" in extensions: + extensions = sorted(ext for ext in self.bot.extensions.keys() if ext not in blacklisted) - await ctx.send(msg) + await ctx.send(self.batch_manage(Action.UNLOAD, *extensions)) @extensions_group.command(name="reload", aliases=("r",)) async def reload_command(self, ctx: Context, *extensions: Extension) -> None: From 161c00979b5dc9ae17a22f55422f3be3c55a1493 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:07:03 -0700 Subject: [PATCH 037/169] Fix: Use a list and sort the extensions that would be loaded with * --- modmail/exts/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index 214af854..b0f5155f 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -182,7 +182,7 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: if "**" in extensions: extensions = self.all_extensions.keys() elif "*" in extensions: - extensions = (self.bot.extensions.keys()).extend(extensions) + extensions = [*extensions, *sorted(self.bot.extensions.keys())] extensions.remove("*") msg = self.batch_manage(Action.RELOAD, *extensions) From ab3f0fb9457694a547d24c8cd232ec0ad5feb26e Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:07:51 -0700 Subject: [PATCH 038/169] Fix: just await in place, remove unneeded variable --- modmail/exts/core.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index b0f5155f..9210eb4a 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -185,9 +185,7 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: extensions = [*extensions, *sorted(self.bot.extensions.keys())] extensions.remove("*") - msg = self.batch_manage(Action.RELOAD, *extensions) - - await ctx.send(msg) + await ctx.send(self.batch_manage(Action.RELOAD, *extensions)) @extensions_group.command(name="list", aliases=("all",)) async def list_command(self, ctx: Context) -> None: From 3952627eadefc947b8b700bc6a67ab3929904c25 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:16:42 -0700 Subject: [PATCH 039/169] Rename Extension -> ExtensionConverter To reduce confusion with the Extension class. It's a real shame discord.py doesn't support registering Converters --- modmail/exts/core.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index 9210eb4a..1b6d4956 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -33,7 +33,7 @@ class Action(Enum): RELOAD = functools.partial(ModmailBot.reload_extension) -class Extension(commands.Converter): +class ExtensionConverter(commands.Converter): """ Fully qualify the name of an extension and ensure it exists. @@ -102,31 +102,21 @@ def wrapper(self: t.Any, *args) -> commands.Command: class ExtensionManager(commands.Cog): """Extension management base class.""" - def __init__(self, bot: ModmailBot, extension_type: str, aliases: t.Optional[t.Tuple[str]] = None): + def __init__(self, bot: ModmailBot): self.bot = bot - self.extension_type = extension_type.lower() - self.aliases = aliases or () - - _all_mapping = {"cog": EXTENSIONS.copy(), "plugin": PLUGINS.copy()} - self.all_extensions = _all_mapping.get(extension_type) - - if not self.all_extensions: - raise ValueError( - f"Looks like you have given an incorrect {extension_type}, " - "valid options are: {', '.join(_all_mapping.keys())}" - ) + self.all_extensions = EXTENSIONS async def get_black_listed_extensions() -> list: """Returns a list of all blacklisted extensions.""" raise NotImplementedError() - @custom_group(invoke_without_command=True) + @commands.group("ext", aliases=("extensions",)) async def extensions_group(self, ctx: Context) -> None: """Load, unload, reload, and list loaded extensions.""" await ctx.send_help(ctx.command) @extensions_group.command(name="load", aliases=("l",)) - async def load_command(self, ctx: Context, *extensions: Extension) -> None: + async def load_command(self, ctx: Context, *extensions: ExtensionConverter) -> None: """ Load extensions given their fully qualified or unqualified names. @@ -143,7 +133,7 @@ async def load_command(self, ctx: Context, *extensions: Extension) -> None: await ctx.send(msg) @extensions_group.command(name="unload", aliases=("ul",)) - async def unload_command(self, ctx: Context, *extensions: Extension) -> None: + async def unload_command(self, ctx: Context, *extensions: ExtensionConverter) -> None: """ Unload currently loaded extensions given their fully qualified or unqualified names. @@ -166,7 +156,7 @@ async def unload_command(self, ctx: Context, *extensions: Extension) -> None: await ctx.send(self.batch_manage(Action.UNLOAD, *extensions)) @extensions_group.command(name="reload", aliases=("r",)) - async def reload_command(self, ctx: Context, *extensions: Extension) -> None: + async def reload_command(self, ctx: Context, *extensions: ExtensionConverter) -> None: """ Reload extensions given their fully qualified or unqualified names. From 1089af99dedecb8777d0f801de603176e2d815e1 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:20:12 -0700 Subject: [PATCH 040/169] Use class attribute for ExtensionConverter extension list --- modmail/exts/core.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index 1b6d4956..26452ae8 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -40,27 +40,25 @@ class ExtensionConverter(commands.Converter): The * and ** values bypass this when used with the reload command. """ + source_list = EXTENSIONS + async def convert(self, ctx: Context, argument: str) -> str: """Fully qualify the name of an extension and ensure it exists.""" - if (ctx.command.name).lower() == "cog": - extensions_all = EXTENSIONS - elif (ctx.command.name).lower() == "plugin": - extensions_all = PLUGINS - # Special values to reload all extensions if argument == "*" or argument == "**": return argument argument = argument.lower() - if argument in extensions_all.keys(): + if argument in self.source_list: return argument - if (qualified_arg := f"{exts.__name__}.{argument}") in extensions_all.keys(): + qualified_arg = f"{exts.__name__}.{argument}" + if qualified_arg in self.source_list: return qualified_arg matches = [] - for ext in extensions_all: + for ext in self.source_list: if argument == unqualify(ext): matches.append(ext) From 3c888d1c426b592e55a78abe62242f975f98a81c Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:22:32 -0700 Subject: [PATCH 041/169] Remove no longer needed custom_group decorator --- modmail/exts/core.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index 26452ae8..539eb258 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -8,7 +8,7 @@ from discord import Colour, Embed from discord.ext import commands -from discord.ext.commands import Context, Group, command +from discord.ext.commands import Context from modmail import exts from modmail.bot import ModmailBot @@ -74,27 +74,9 @@ async def convert(self, ctx: Context, argument: str) -> str: return matches[0] -def custom_group() -> t.Callable: - """ - Custom command `group` decorator. - - Reads the `name` and `alias` attributes from the decorator and passes it on to the group. - """ - def decorator(function: t.Callable) -> t.Callable: - @functools.wraps(function) - def wrapper(self: t.Any, *args) -> commands.Command: - args.setdefault("cls", Group) - return command( - name=self.extension_type, - aliases=self.aliases, - help=f"Load, unload, reload, and list loaded {self.extension_type}.", - **args, - ) - return wrapper - return decorator class ExtensionManager(commands.Cog): From 25b616bdf63ffcc4ab8178f80c5a9b6f8672d663 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:24:16 -0700 Subject: [PATCH 042/169] Refactor ExtensionConverter to return early and add PluginConverter Maybe now flake8 will be quiet about unused imports --- modmail/exts/core.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index 539eb258..b935bcd4 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -41,6 +41,7 @@ class ExtensionConverter(commands.Converter): """ source_list = EXTENSIONS + type = "extension" async def convert(self, ctx: Context, argument: str) -> str: """Fully qualify the name of an extension and ensure it exists.""" @@ -63,20 +64,27 @@ async def convert(self, ctx: Context, argument: str) -> str: matches.append(ext) if not matches: - raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") - elif len(matches) > 1: + raise commands.BadArgument(f":x: Could not find the {self.type} `{argument}`.") + + if len(matches) > 1: names = "\n".join(sorted(matches)) raise commands.BadArgument( - f":x: `{argument}` is an ambiguous extension name. " + f":x: `{argument}` is an ambiguous {self.type} name. " f"Please use one of the following fully-qualified names.```\n{names}```" ) - else: - return matches[0] + return matches[0] +class PluginConverter(ExtensionConverter): + """ + Fully qualify the name of a plugin and ensure it exists. + The * and ** values bypass this when used with the reload command. + """ + source_list = PLUGINS + type = "plugin" class ExtensionManager(commands.Cog): @@ -180,7 +188,7 @@ async def list_command(self, ctx: Context) -> None: lines.append(f"**{category}**\n{extensions}\n") log.debug( - f"{ctx.author} requested a list of all {self.extension_type.lower()}s. " + f"{ctx.author} requested a list of all extensions. " "Returning a paginated list." ) From 9f831e424c855db25b7343b13827799c017fbfe5 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:26:28 -0700 Subject: [PATCH 043/169] Add pluginmanager class that's incomplete --- modmail/exts/core.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index b935bcd4..54d9356d 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -290,13 +290,17 @@ class PluginsManager(ExtensionManager): """Plugin management commands.""" def __init__(self, bot: ModmailBot) -> None: - self.bot = bot + super().__init__(bot) + + @commands.group("plugins", aliases=("plug", "plugs", "plugins")) + async def plugins_group(self, ctx: Context) -> None: + """Install, uninstall, disable, update, and enable installed plugins.""" + await ctx.send_help(ctx.command) - _extension_type = "plugin" - _aliases = ("plug", "plugs", "plugins") - ExtensionManager.__init__(self, bot, _extension_type, _aliases) + # Not implemented def setup(bot: ModmailBot) -> None: """Load the Plugins manager cog.""" + bot.add_cog(ExtensionManager(bot)) bot.add_cog(PluginsManager(bot)) From 3198d44cd42362d3d41216a12babff308b4a198a Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:26:34 -0700 Subject: [PATCH 044/169] Ignore: autoformat --- modmail/exts/core.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index 54d9356d..55d47829 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -187,10 +187,7 @@ async def list_command(self, ctx: Context) -> None: extensions = "\n".join(sorted(extensions)) lines.append(f"**{category}**\n{extensions}\n") - log.debug( - f"{ctx.author} requested a list of all extensions. " - "Returning a paginated list." - ) + log.debug(f"{ctx.author} requested a list of all extensions. " "Returning a paginated list.") # since we currently don't have a paginator. await ctx.send("".join(lines)) From 9ca49ea8e0894efe76b5cb05a6b1918ffb836a5e Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:39:23 -0700 Subject: [PATCH 045/169] Genericize ExtensionManager and rename all the command functions --- modmail/exts/core.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index 55d47829..056c1788 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -90,6 +90,8 @@ class PluginConverter(ExtensionConverter): class ExtensionManager(commands.Cog): """Extension management base class.""" + type = "extension" + def __init__(self, bot: ModmailBot): self.bot = bot self.all_extensions = EXTENSIONS @@ -104,7 +106,7 @@ async def extensions_group(self, ctx: Context) -> None: await ctx.send_help(ctx.command) @extensions_group.command(name="load", aliases=("l",)) - async def load_command(self, ctx: Context, *extensions: ExtensionConverter) -> None: + async def load_extensions(self, ctx: Context, *extensions: ExtensionConverter) -> None: """ Load extensions given their fully qualified or unqualified names. @@ -121,7 +123,7 @@ async def load_command(self, ctx: Context, *extensions: ExtensionConverter) -> N await ctx.send(msg) @extensions_group.command(name="unload", aliases=("ul",)) - async def unload_command(self, ctx: Context, *extensions: ExtensionConverter) -> None: + async def unload_extensions(self, ctx: Context, *extensions: ExtensionConverter) -> None: """ Unload currently loaded extensions given their fully qualified or unqualified names. @@ -135,7 +137,7 @@ async def unload_command(self, ctx: Context, *extensions: ExtensionConverter) -> if blacklisted: bl_msg = "\n".join(blacklisted) - await ctx.send(f":x: The following extension(s) may not be unloaded:```\n{bl_msg}```") + await ctx.send(f":x: The following {self.type}(s) may not be unloaded:```\n{bl_msg}```") return if "*" in extensions or "**" in extensions: @@ -144,7 +146,7 @@ async def unload_command(self, ctx: Context, *extensions: ExtensionConverter) -> await ctx.send(self.batch_manage(Action.UNLOAD, *extensions)) @extensions_group.command(name="reload", aliases=("r",)) - async def reload_command(self, ctx: Context, *extensions: ExtensionConverter) -> None: + async def reload_extensions(self, ctx: Context, *extensions: ExtensionConverter) -> None: """ Reload extensions given their fully qualified or unqualified names. @@ -166,7 +168,7 @@ async def reload_command(self, ctx: Context, *extensions: ExtensionConverter) -> await ctx.send(self.batch_manage(Action.RELOAD, *extensions)) @extensions_group.command(name="list", aliases=("all",)) - async def list_command(self, ctx: Context) -> None: + async def list_extensions(self, ctx: Context) -> None: """ Get a list of all extensions, including their loaded status. @@ -175,7 +177,7 @@ async def list_command(self, ctx: Context) -> None: """ embed = Embed(colour=Colour.blurple()) embed.set_author( - name="Extensions List", + name=f"{self.type.capitalize()} List", ) lines = [] @@ -187,7 +189,7 @@ async def list_command(self, ctx: Context) -> None: extensions = "\n".join(sorted(extensions)) lines.append(f"**{category}**\n{extensions}\n") - log.debug(f"{ctx.author} requested a list of all extensions. " "Returning a paginated list.") + log.debug(f"{ctx.author} requested a list of all {self.type}s. " "Returning a paginated list.") # since we currently don't have a paginator. await ctx.send("".join(lines)) @@ -231,13 +233,13 @@ def batch_manage(self, action: Action, *extensions: str) -> str: failures[extension] = error emoji = ":x:" if failures else ":ok_hand:" - msg = f"{emoji} {len(extensions) - len(failures)} / {len(extensions)} extensions {verb}ed." + msg = f"{emoji} {len(extensions) - len(failures)} / {len(extensions)} {self.type}s {verb}ed." if failures: failures = "\n".join(f"{ext}\n {err}" for ext, err in failures.items()) msg += f"\nFailures:```\n{failures}```" - log.debug(f"Batch {verb}ed extensions.") + log.debug(f"Batch {verb}ed {self.type}s.") return msg @@ -254,18 +256,18 @@ def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: log.debug("Treating {ext!r} as if it was not loaded.") return self.manage(Action.LOAD, ext) - msg = f":x: Extension `{ext}` is already {verb}ed." + msg = f":x: {self.type.capitalize()} `{ext}` is already {verb}ed." log.debug(msg[4:]) except Exception as e: if hasattr(e, "original"): e = e.original - log.exception(f"Extension '{ext}' failed to {verb}.") + log.exception(f"{self.type.capitalize()} '{ext}' failed to {verb}.") error_msg = f"{e.__class__.__name__}: {e}" - msg = f":x: Failed to {verb} extension `{ext}`:\n```\n{error_msg}```" + msg = f":x: Failed to {verb} {self.type} `{ext}`:\n```\n{error_msg}```" else: - msg = f":ok_hand: Extension successfully {verb}ed: `{ext}`." + msg = f":ok_hand: {self.type.capitalize()} successfully {verb}ed: `{ext}`." log.debug(msg[10:]) return msg, error_msg From 5f837b467fe1c9b3cf920c46c3dca2b1491eb7ea Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:44:09 -0700 Subject: [PATCH 046/169] Implement PluginManager most of the way --- modmail/exts/core.py | 51 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index 056c1788..ed214c63 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -285,21 +285,64 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: error.handled = True -class PluginsManager(ExtensionManager): +class PluginManager(ExtensionManager): """Plugin management commands.""" + type = "plugin" + def __init__(self, bot: ModmailBot) -> None: super().__init__(bot) + self.all_extensions = PLUGINS @commands.group("plugins", aliases=("plug", "plugs", "plugins")) async def plugins_group(self, ctx: Context) -> None: """Install, uninstall, disable, update, and enable installed plugins.""" await ctx.send_help(ctx.command) - # Not implemented + @plugins_group.command(name="load", aliases=("l",)) + async def load_plugin(self, ctx: Context, *plugins: PluginConverter) -> None: + """ + Load plugins given their fully qualified or unqualified names. + + If '\*' or '\*\*' is given as the name, all unloaded plugins will be loaded. + """ # noqa: W605 + await self.load_extensions(ctx, *plugins) + + @plugins_group.command(name="unload", aliases=("ul",)) + async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: + """ + Unload currently loaded plugins given their fully qualified or unqualified names. + + If '\*' or '\*\*' is given as the name, all loaded plugins will be unloaded. + """ # noqa: W605 + await self.unload_extensions(ctx, *plugins) + + @plugins_group.command(name="reload", aliases=("r",)) + async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: + """ + Reload extensions given their fully qualified or unqualified names. + + If an extension fails to be reloaded, it will be rolled-back to the prior working state. + + If '*' is given as the name, all currently loaded extensions will be reloaded. + If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. + """ + await self.reload_extensions(ctx, *plugins) + + @plugins_group.command(name="list", aliases=("all",)) + async def list_plugins(self, ctx: Context) -> None: + """ + Get a list of all extensions, including their loaded status. + + Grey indicates that the extension is unloaded. + Green indicates that the extension is currently loaded. + """ + await self.list_extensions(ctx) + + # TODO: Implement install/enable/disable/etc def setup(bot: ModmailBot) -> None: """Load the Plugins manager cog.""" - bot.add_cog(ExtensionManager(bot)) - bot.add_cog(PluginsManager(bot)) + # PluginManager includes the ExtensionManager + bot.add_cog(PluginManager(bot)) From 383c23fb28f10bcd3f804d37f3e0939ee2589078 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:45:51 -0700 Subject: [PATCH 047/169] Delete cogsmanager We do not need three different managers--extensions works for internal, plugins for special external ones that are supposed to be extra hotpluggable --- modmail/exts/extensions/_plugins.py | 0 modmail/exts/extensions/cogs.py | 18 ------------------ 2 files changed, 18 deletions(-) delete mode 100644 modmail/exts/extensions/_plugins.py delete mode 100644 modmail/exts/extensions/cogs.py diff --git a/modmail/exts/extensions/_plugins.py b/modmail/exts/extensions/_plugins.py deleted file mode 100644 index e69de29b..00000000 diff --git a/modmail/exts/extensions/cogs.py b/modmail/exts/extensions/cogs.py deleted file mode 100644 index ab3cd1ed..00000000 --- a/modmail/exts/extensions/cogs.py +++ /dev/null @@ -1,18 +0,0 @@ -from modmail.bot import ModmailBot -from modmail.exts.extensions._base_class import ExtensionManager - - -class CogsManager(ExtensionManager): - """Cogs management commands.""" - - def __init__(self, bot: ModmailBot) -> None: - self.bot = bot - - _extension_type = "cog" - _aliases = ("ext", "exts", "c", "cogs") - ExtensionManager.__init__(self, bot, _extension_type, _aliases) - - -def setup(bot: ModmailBot) -> None: - """Load the Cogs manager cog.""" - bot.add_cog(CogsManager(bot)) From 464a0a169a5c78a56498fedaed57eb29cd583f91 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:50:54 -0700 Subject: [PATCH 048/169] Use `or` for nice debug spot --- modmail/exts/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modmail/exts/core.py b/modmail/exts/core.py index ed214c63..7f058d06 100644 --- a/modmail/exts/core.py +++ b/modmail/exts/core.py @@ -257,7 +257,6 @@ def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: return self.manage(Action.LOAD, ext) msg = f":x: {self.type.capitalize()} `{ext}` is already {verb}ed." - log.debug(msg[4:]) except Exception as e: if hasattr(e, "original"): e = e.original @@ -268,8 +267,8 @@ def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: msg = f":x: Failed to {verb} {self.type} `{ext}`:\n```\n{error_msg}```" else: msg = f":ok_hand: {self.type.capitalize()} successfully {verb}ed: `{ext}`." - log.debug(msg[10:]) + log.debug(error_msg or msg) return msg, error_msg # This cannot be static (must have a __func__ attribute). From b6f42b066f4d6a85f401d8e1df111055819a6fcb Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:55:06 -0700 Subject: [PATCH 049/169] Support 3.7/3.8 by stripping walrus operator --- modmail/utils/extensions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 46329839..c27578f8 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -47,7 +47,8 @@ def on_error(name: str) -> NoReturn: # If it lacks a setup function, it's not an extension. continue - if (ext_metadata := getattr(imported, "EXT_METADATA", None)) is not None: + ext_metadata = getattr(imported, "EXT_METADATA", None) + if ext_metadata is not None: # check if this cog is dev only or plugin dev only load_cog = bool(calc_mode(ext_metadata) & BOT_MODE) log.trace(f"Load cog {module.name!r}?: {load_cog}") From 476bf4a55bca15787ce0c641f0943d021b3ad96a Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:55:21 -0700 Subject: [PATCH 050/169] Note that module-based cog load/unload is enforced by dpy --- modmail/utils/plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py index 678af91b..9b86bd94 100644 --- a/modmail/utils/plugin_manager.py +++ b/modmail/utils/plugin_manager.py @@ -47,7 +47,7 @@ def walk_plugins() -> Iterator[str]: spec.loader.exec_module(imported) if not inspect.isfunction(getattr(imported, "setup", None)): - # If it lacks a setup function, it's not an plugin. + # If it lacks a setup function, it's not a plugin. This is enforced by dpy. continue if (ext_metadata := getattr(imported, "EXT_METADATA", None)) is not None: From 14c70685a2710497ecd6cddbba1f1e9a35eada60 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 22:56:09 -0700 Subject: [PATCH 051/169] Nit: Remove redundant parens --- modmail/utils/extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index c27578f8..f0e6e353 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -58,7 +58,7 @@ def on_error(name: str) -> NoReturn: log.notice(f"Cog {module.name!r} is missing an EXT_METADATA variable. Assuming its a normal cog.") # Presume Production Mode - yield (module.name, True) + yield module.name, True EXTENSIONS = dict() From 407b29011e1e684cf590423afad5cff163a0d024 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:00:57 -0700 Subject: [PATCH 052/169] Move plugins underneath extensions folder for better organization --- modmail/{ => exts}/plugins/.gitignore | 0 modmail/{ => exts}/plugins/__init__.py | 0 modmail/{ => exts}/plugins/local/README.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename modmail/{ => exts}/plugins/.gitignore (100%) rename modmail/{ => exts}/plugins/__init__.py (100%) rename modmail/{ => exts}/plugins/local/README.md (100%) diff --git a/modmail/plugins/.gitignore b/modmail/exts/plugins/.gitignore similarity index 100% rename from modmail/plugins/.gitignore rename to modmail/exts/plugins/.gitignore diff --git a/modmail/plugins/__init__.py b/modmail/exts/plugins/__init__.py similarity index 100% rename from modmail/plugins/__init__.py rename to modmail/exts/plugins/__init__.py diff --git a/modmail/plugins/local/README.md b/modmail/exts/plugins/local/README.md similarity index 100% rename from modmail/plugins/local/README.md rename to modmail/exts/plugins/local/README.md From a024097eed4f9b7177757e66f2ce9372d81c412e Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:01:50 -0700 Subject: [PATCH 053/169] Move extensions and plugins data directories to root modmail folder --- modmail/{exts => }/extensions/__init__.py | 0 modmail/{exts => }/plugins/.gitignore | 0 modmail/{exts => }/plugins/__init__.py | 0 modmail/{exts => }/plugins/local/README.md | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename modmail/{exts => }/extensions/__init__.py (100%) rename modmail/{exts => }/plugins/.gitignore (100%) rename modmail/{exts => }/plugins/__init__.py (100%) rename modmail/{exts => }/plugins/local/README.md (100%) diff --git a/modmail/exts/extensions/__init__.py b/modmail/extensions/__init__.py similarity index 100% rename from modmail/exts/extensions/__init__.py rename to modmail/extensions/__init__.py diff --git a/modmail/exts/plugins/.gitignore b/modmail/plugins/.gitignore similarity index 100% rename from modmail/exts/plugins/.gitignore rename to modmail/plugins/.gitignore diff --git a/modmail/exts/plugins/__init__.py b/modmail/plugins/__init__.py similarity index 100% rename from modmail/exts/plugins/__init__.py rename to modmail/plugins/__init__.py diff --git a/modmail/exts/plugins/local/README.md b/modmail/plugins/local/README.md similarity index 100% rename from modmail/exts/plugins/local/README.md rename to modmail/plugins/local/README.md From 339687a04c4503c42e31c7aac171ad50383474ce Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:02:24 -0700 Subject: [PATCH 054/169] Support python 3.7/3.8 and not use walrus for plugin loading --- modmail/utils/plugin_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py index 9b86bd94..4b6fed2c 100644 --- a/modmail/utils/plugin_manager.py +++ b/modmail/utils/plugin_manager.py @@ -50,7 +50,8 @@ def walk_plugins() -> Iterator[str]: # If it lacks a setup function, it's not a plugin. This is enforced by dpy. continue - if (ext_metadata := getattr(imported, "EXT_METADATA", None)) is not None: + ext_metadata = getattr(imported, "EXT_METADATA", None) + if ext_metadata is not None: # check if this plugin is dev only or plugin dev only load_cog = calc_mode(ext_metadata & BOT_MODE) log.trace(f"Load plugin {imported.__name__!r}?: {load_cog}") From 1a51fbabbea6f7ba826beac00f7650b166cd3ff8 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:03:16 -0700 Subject: [PATCH 055/169] Nit: remove redundant parens --- modmail/utils/plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py index 4b6fed2c..f55ba71d 100644 --- a/modmail/utils/plugin_manager.py +++ b/modmail/utils/plugin_manager.py @@ -62,7 +62,7 @@ def walk_plugins() -> Iterator[str]: f"Plugin {imported.__name__!r} is missing a EXT_METADATA variable. Assuming its a normal plugin." ) - yield (imported.__name__, True) + yield imported.__name__, True PLUGINS = dict(walk_plugins()) From 0cbc54b2a8e66a21f4a273b5be02d710f993f5e7 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:05:26 -0700 Subject: [PATCH 056/169] Alter extension loading to be more like plugin loading and match new paths --- modmail/utils/extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index f0e6e353..e4919214 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -7,7 +7,7 @@ import pkgutil from typing import Iterator, NoReturn -from modmail import exts +from modmail import extensions from modmail.config import CONFIG from modmail.log import ModmailLogger from modmail.utils.cogs import BOT_MODES, calc_mode @@ -30,7 +30,7 @@ def walk_extensions() -> Iterator[str]: def on_error(name: str) -> NoReturn: raise ImportError(name=name) # pragma: no cover - for module in pkgutil.walk_packages(exts.__path__, f"{exts.__name__}.", onerror=on_error): + for module in pkgutil.walk_packages(extensions.__path__, f"{extensions.__name__}.", onerror=on_error): if unqualify(module.name).startswith("_"): # Ignore module/package names starting with an underscore. continue From f3ce4af0b94344f5f0cd25b96ce1b01da1593f71 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sat, 7 Aug 2021 02:09:58 -0400 Subject: [PATCH 057/169] gh intergration: make codecov informational only --- .codecov.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.codecov.yml b/.codecov.yml index c964e104..9e4bea5e 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -35,7 +35,8 @@ coverage: # advanced branches: - main - if_ci_failed: error #success, failure, error, ignore + if_ci_failed: ignore #success, failure, error, ignore + informational: true only_pulls: true From f860dd79c5dd1e1b8f10e66fd5d9a42e995ee1c8 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:13:25 -0700 Subject: [PATCH 058/169] Fix: don't instaload plugins, this will guarantee an import cycle --- modmail/utils/plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py index f55ba71d..ed7e8cf9 100644 --- a/modmail/utils/plugin_manager.py +++ b/modmail/utils/plugin_manager.py @@ -65,4 +65,4 @@ def walk_plugins() -> Iterator[str]: yield imported.__name__, True -PLUGINS = dict(walk_plugins()) +PLUGINS = dict() From 205db2a96b6c7721db98016f59df8f4771e19870 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:13:33 -0700 Subject: [PATCH 059/169] Load plugins after extensions --- modmail/__main__.py | 1 + modmail/bot.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/modmail/__main__.py b/modmail/__main__.py index e9769cfc..81af6fb4 100644 --- a/modmail/__main__.py +++ b/modmail/__main__.py @@ -17,6 +17,7 @@ def main() -> None: """Run the bot.""" bot = ModmailBot() bot.load_extensions() + bot.load_plugins() log.notice("Running the bot.") bot.run(bot.config.bot.token) diff --git a/modmail/bot.py b/modmail/bot.py index 69dd1a88..0185dd2e 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -59,6 +59,15 @@ def load_extensions(self) -> None: if should_load: self.load_extension(extension) + def load_plugins(self) -> None: + """Load all enabled plugins.""" + from modmail.utils.plugin_manager import PLUGINS, walk_plugins + + PLUGINS.update(walk_plugins()) + for extension, should_load in PLUGINS.items(): + if should_load: + self.load_extension(extension) + def add_cog(self, cog: commands.Cog) -> None: """ Delegate to super to register `cog`. From f2ea06dd587848766676d28316934d6d54fc9122 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:14:59 -0700 Subject: [PATCH 060/169] Move from global log to class logger on ModmailBot --- modmail/bot.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 0185dd2e..d2535af5 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -7,8 +7,6 @@ from modmail.config import CONFIG, INTERNAL -log = logging.getLogger(__name__) - class ModmailBot(commands.Bot): """ @@ -18,6 +16,7 @@ class ModmailBot(commands.Bot): """ main_task: asyncio.Task + logger = logging.getLogger(__name__) def __init__(self, **kwargs): self.config = CONFIG @@ -36,13 +35,13 @@ async def close(self) -> None: try: self.unload_extension(ext) except Exception: - log.error(f"Exception occured while unloading {ext.name}", exc_info=1) + self.logger.error(f"Exception occured while unloading {ext.name}", exc_info=1) for cog in list(self.cogs): try: self.remove_cog(cog) except Exception: - log.error(f"Exception occured while removing cog {cog.name}", exc_info=1) + self.logger.error(f"Exception occured while removing cog {cog.name}", exc_info=1) if self.http_session: await self.http_session.close() @@ -75,7 +74,7 @@ def add_cog(self, cog: commands.Cog) -> None: This only serves to make the info log, so that extensions don't have to. """ super().add_cog(cog) - log.info(f"Cog loaded: {cog.qualified_name}") + self.logger.info(f"Cog loaded: {cog.qualified_name}") def remove_cog(self, cog: commands.Cog) -> None: """ @@ -84,8 +83,8 @@ def remove_cog(self, cog: commands.Cog) -> None: This only serves to make the debug log, so that extensions don't have to. """ super().remove_cog(cog) - log.trace(f"Cog unloaded: {cog}") + self.logger.trace(f"Cog unloaded: {cog}") async def on_ready(self) -> None: """Send basic login success message.""" - log.info("Logged in as %s", self.user) + self.logger.info("Logged in as %s", self.user) From 5e4c17975eaa2eec932338ee927b84d14a79dd31 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:15:31 -0700 Subject: [PATCH 061/169] Debug log when extensions and plugins are initially loaded --- modmail/bot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index d2535af5..0d6f7d70 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -56,6 +56,7 @@ def load_extensions(self) -> None: EXTENSIONS.update(walk_extensions()) for extension, should_load in EXTENSIONS.items(): if should_load: + self.logger.debug(f"Loading extension {extension.name}") self.load_extension(extension) def load_plugins(self) -> None: @@ -63,9 +64,10 @@ def load_plugins(self) -> None: from modmail.utils.plugin_manager import PLUGINS, walk_plugins PLUGINS.update(walk_plugins()) - for extension, should_load in PLUGINS.items(): + for plugin, should_load in PLUGINS.items(): if should_load: - self.load_extension(extension) + self.logger.debug(f"Loading plugin {plugin.name}") + self.load_extension(plugin) def add_cog(self, cog: commands.Cog) -> None: """ From 5ac76ddb6612fc455b4dbaa8ca3f2f37c1f03dfd Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:21:19 -0700 Subject: [PATCH 062/169] Move core and meta into the extensions folder proper --- modmail/{exts => extensions}/core.py | 0 modmail/{exts => extensions}/meta.py | 0 modmail/exts/__init__.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename modmail/{exts => extensions}/core.py (100%) rename modmail/{exts => extensions}/meta.py (100%) delete mode 100644 modmail/exts/__init__.py diff --git a/modmail/exts/core.py b/modmail/extensions/core.py similarity index 100% rename from modmail/exts/core.py rename to modmail/extensions/core.py diff --git a/modmail/exts/meta.py b/modmail/extensions/meta.py similarity index 100% rename from modmail/exts/meta.py rename to modmail/extensions/meta.py diff --git a/modmail/exts/__init__.py b/modmail/exts/__init__.py deleted file mode 100644 index e69de29b..00000000 From 11f50476852c4f64ebb38b8e857d7b5a88ae53ac Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:22:33 -0700 Subject: [PATCH 063/169] Remove dependency of automated extensions path name --- modmail/extensions/core.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 7f058d06..91ba549c 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -10,7 +10,6 @@ from discord.ext import commands from discord.ext.commands import Context -from modmail import exts from modmail.bot import ModmailBot from modmail.log import ModmailLogger from modmail.utils.cogs import ExtMetadata @@ -19,7 +18,8 @@ log: ModmailLogger = logging.getLogger(__name__) -BASE_PATH = exts.__name__.count(".") + 1 + +BASE_PATH_LEN = __name__.count(".") EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) @@ -54,7 +54,7 @@ async def convert(self, ctx: Context, argument: str) -> str: if argument in self.source_list: return argument - qualified_arg = f"{exts.__name__}.{argument}" + qualified_arg = f"modmail.{self.type}s.{argument}" if qualified_arg in self.source_list: return qualified_arg @@ -205,8 +205,8 @@ def group_extension_statuses(self) -> t.Mapping[str, str]: status = ":red_circle:" root, name = ext.rsplit(".", 1) - if len(root) > len(BASE_PATH): - category = " - ".join(root[len(BASE_PATH) + 1 :].split(".")) + if len(root) > BASE_PATH_LEN: + category = " - ".join(root[BASE_PATH_LEN:].split(".")) else: category = "uncategorized" From 734b045e5daa4704248ae058cb1729b1aec2b0d5 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:22:49 -0700 Subject: [PATCH 064/169] Fix: nameerror because loading plugins just has the name not the actual object --- modmail/bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 0d6f7d70..8817a086 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -56,7 +56,7 @@ def load_extensions(self) -> None: EXTENSIONS.update(walk_extensions()) for extension, should_load in EXTENSIONS.items(): if should_load: - self.logger.debug(f"Loading extension {extension.name}") + self.logger.debug(f"Loading extension {extension}") self.load_extension(extension) def load_plugins(self) -> None: @@ -66,7 +66,7 @@ def load_plugins(self) -> None: PLUGINS.update(walk_plugins()) for plugin, should_load in PLUGINS.items(): if should_load: - self.logger.debug(f"Loading plugin {plugin.name}") + self.logger.debug(f"Loading plugin {plugin}") self.load_extension(plugin) def add_cog(self, cog: commands.Cog) -> None: From 5c505963738cae0d2753004e24b81edba3b1cb3d Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:25:43 -0700 Subject: [PATCH 065/169] Clean up mode calculation --- modmail/utils/cogs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index 31fc78c6..8e60cac1 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -43,9 +43,9 @@ class ExtMetadata: def calc_mode(metadata: ExtMetadata) -> int: """Calculate the combination of different variables and return the binary combination.""" - mode = int(getattr(metadata, "production", False)) - mode += int(getattr(metadata, "develop", False) << 1) or 0 - mode = mode + (int(getattr(metadata, "plugin_dev", False)) << 2) + mode = getattr(metadata, "production", False) + mode += getattr(metadata, "develop", False) << 1 + mode += getattr(metadata, "plugin_dev", False) << 2 return mode From 0aab6b1878bc384723a2854cc156af2015fabca6 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:39:55 -0700 Subject: [PATCH 066/169] Give Meta EXT_METADATA --- modmail/extensions/meta.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modmail/extensions/meta.py b/modmail/extensions/meta.py index 8853f6f8..f09b95a3 100644 --- a/modmail/extensions/meta.py +++ b/modmail/extensions/meta.py @@ -5,12 +5,14 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import BotModes +from modmail.utils.cogs import BotModes, ExtMetadata log: ModmailLogger = logging.getLogger(__name__) print(BotModes.plugin_dev) +EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) + class Meta(commands.Cog): """Meta commands to get info about the bot itself.""" From 2667e52a43230e76a811c8a52f5d20a24e1a0aef Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:40:13 -0700 Subject: [PATCH 067/169] Use defaultdict to clean up getting group categories --- modmail/extensions/core.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 91ba549c..c03fb06d 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -4,6 +4,7 @@ import functools import logging import typing as t +from collections import defaultdict from enum import Enum from discord import Colour, Embed @@ -196,9 +197,9 @@ async def list_extensions(self, ctx: Context) -> None: def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" - categories = {} + categories = defaultdict(list) - for ext in self.all_extensions.keys(): + for ext in self.all_extensions: if ext in self.bot.extensions: status = ":green_circle:" else: @@ -211,8 +212,9 @@ def group_extension_statuses(self) -> t.Mapping[str, str]: category = "uncategorized" categories.setdefault(category, []).append(f"{status} {name}") + categories[category].append(f"{status} {name}") - return categories + return dict(categories) def batch_manage(self, action: Action, *extensions: str) -> str: """ From 1a45de9410991d064a156f6bb5020c71caaf4d43 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:40:29 -0700 Subject: [PATCH 068/169] Just use full extension path for modules for now --- modmail/extensions/core.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index c03fb06d..cbec925a 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -206,12 +206,9 @@ def group_extension_statuses(self) -> t.Mapping[str, str]: status = ":red_circle:" root, name = ext.rsplit(".", 1) - if len(root) > BASE_PATH_LEN: - category = " - ".join(root[BASE_PATH_LEN:].split(".")) - else: - category = "uncategorized" - categories.setdefault(category, []).append(f"{status} {name}") + category = " - ".join(root.split(".")) + categories[category].append(f"{status} {name}") return dict(categories) From cc6d0f948031317aee532e2ec2b42a596ec5dcc0 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:40:52 -0700 Subject: [PATCH 069/169] Fix plugin cog failing to load because a command was given itself as an alias --- modmail/extensions/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index cbec925a..278b7519 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -292,7 +292,7 @@ def __init__(self, bot: ModmailBot) -> None: super().__init__(bot) self.all_extensions = PLUGINS - @commands.group("plugins", aliases=("plug", "plugs", "plugins")) + @commands.group("plugins", aliases=("plug", "plugs")) async def plugins_group(self, ctx: Context) -> None: """Install, uninstall, disable, update, and enable installed plugins.""" await ctx.send_help(ctx.command) From 79984db88cdbde774d927cc936306d8f9ae8b233 Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:41:16 -0700 Subject: [PATCH 070/169] Fix dpy doing things with inheritance when we just want inheritance Hi python2, good to see you again --- modmail/extensions/core.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 278b7519..59a46453 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -283,13 +283,15 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: error.handled = True -class PluginManager(ExtensionManager): +class PluginManager(commands.Cog): """Plugin management commands.""" type = "plugin" def __init__(self, bot: ModmailBot) -> None: - super().__init__(bot) + # We don't use super because discord.py uses superclasses for something + # and we just want regular inheritance + ExtensionManager.__init__(self, bot) self.all_extensions = PLUGINS @commands.group("plugins", aliases=("plug", "plugs")) @@ -304,7 +306,7 @@ async def load_plugin(self, ctx: Context, *plugins: PluginConverter) -> None: If '\*' or '\*\*' is given as the name, all unloaded plugins will be loaded. """ # noqa: W605 - await self.load_extensions(ctx, *plugins) + await ExtensionManager.load_extensions(self, ctx, *plugins) @plugins_group.command(name="unload", aliases=("ul",)) async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: @@ -313,7 +315,7 @@ async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: If '\*' or '\*\*' is given as the name, all loaded plugins will be unloaded. """ # noqa: W605 - await self.unload_extensions(ctx, *plugins) + await ExtensionManager.unload_extensions(self, ctx, *plugins) @plugins_group.command(name="reload", aliases=("r",)) async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: @@ -325,7 +327,7 @@ async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: If '*' is given as the name, all currently loaded extensions will be reloaded. If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. """ - await self.reload_extensions(ctx, *plugins) + await ExtensionManager.reload_extensions(self, ctx, *plugins) @plugins_group.command(name="list", aliases=("all",)) async def list_plugins(self, ctx: Context) -> None: @@ -335,7 +337,7 @@ async def list_plugins(self, ctx: Context) -> None: Grey indicates that the extension is unloaded. Green indicates that the extension is currently loaded. """ - await self.list_extensions(ctx) + await ExtensionManager.list_extensions(self, ctx) # TODO: Implement install/enable/disable/etc @@ -343,4 +345,5 @@ async def list_plugins(self, ctx: Context) -> None: def setup(bot: ModmailBot) -> None: """Load the Plugins manager cog.""" # PluginManager includes the ExtensionManager + bot.add_cog(ExtensionManager(bot)) bot.add_cog(PluginManager(bot)) From 06f0f1a121fdd41747647696d3c70f9a0ea526db Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:52:14 -0700 Subject: [PATCH 071/169] Use invoke_without_command to avoid group help being printed for every command Co-authored-by: onerandomusername --- modmail/extensions/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 59a46453..2ce82ed3 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -101,7 +101,7 @@ async def get_black_listed_extensions() -> list: """Returns a list of all blacklisted extensions.""" raise NotImplementedError() - @commands.group("ext", aliases=("extensions",)) + @commands.group("ext", aliases=("extensions",), invoke_without_command=True) async def extensions_group(self, ctx: Context) -> None: """Load, unload, reload, and list loaded extensions.""" await ctx.send_help(ctx.command) @@ -294,7 +294,7 @@ def __init__(self, bot: ModmailBot) -> None: ExtensionManager.__init__(self, bot) self.all_extensions = PLUGINS - @commands.group("plugins", aliases=("plug", "plugs")) + @commands.group("plugins", aliases=("plug", "plugs"), invoke_without_command=True) async def plugins_group(self, ctx: Context) -> None: """Install, uninstall, disable, update, and enable installed plugins.""" await ctx.send_help(ctx.command) From bda1c0d3b2a77ddb7800be8bf57c61261cbec70e Mon Sep 17 00:00:00 2001 From: Bast Date: Fri, 6 Aug 2021 23:53:21 -0700 Subject: [PATCH 072/169] Return to inheritance now that we've figured out how to purge dpy's command inheritance --- modmail/extensions/core.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 2ce82ed3..3a990a18 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -283,7 +283,7 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: error.handled = True -class PluginManager(commands.Cog): +class PluginManager(ExtensionManager): """Plugin management commands.""" type = "plugin" @@ -291,7 +291,7 @@ class PluginManager(commands.Cog): def __init__(self, bot: ModmailBot) -> None: # We don't use super because discord.py uses superclasses for something # and we just want regular inheritance - ExtensionManager.__init__(self, bot) + super().__init__(bot) self.all_extensions = PLUGINS @commands.group("plugins", aliases=("plug", "plugs"), invoke_without_command=True) @@ -306,7 +306,7 @@ async def load_plugin(self, ctx: Context, *plugins: PluginConverter) -> None: If '\*' or '\*\*' is given as the name, all unloaded plugins will be loaded. """ # noqa: W605 - await ExtensionManager.load_extensions(self, ctx, *plugins) + await self.load_extensions(ctx, *plugins) @plugins_group.command(name="unload", aliases=("ul",)) async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: @@ -315,7 +315,7 @@ async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: If '\*' or '\*\*' is given as the name, all loaded plugins will be unloaded. """ # noqa: W605 - await ExtensionManager.unload_extensions(self, ctx, *plugins) + await self.unload_extensions(ctx, *plugins) @plugins_group.command(name="reload", aliases=("r",)) async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: @@ -327,7 +327,7 @@ async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: If '*' is given as the name, all currently loaded extensions will be reloaded. If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. """ - await ExtensionManager.reload_extensions(self, ctx, *plugins) + await self.reload_extensions(ctx, *plugins) @plugins_group.command(name="list", aliases=("all",)) async def list_plugins(self, ctx: Context) -> None: @@ -337,11 +337,17 @@ async def list_plugins(self, ctx: Context) -> None: Grey indicates that the extension is unloaded. Green indicates that the extension is currently loaded. """ - await ExtensionManager.list_extensions(self, ctx) + await self.list_extensions(ctx) # TODO: Implement install/enable/disable/etc +# Delete the commands from ExtensionManager before +# discord.py tries to screw up by reregistering them +for command in ExtensionManager.__cog_commands__: + PluginManager.__cog_commands__.remove(command) + + def setup(bot: ModmailBot) -> None: """Load the Plugins manager cog.""" # PluginManager includes the ExtensionManager From a3aa272798901c567542ecc656e9df5031318289 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sat, 7 Aug 2021 02:59:27 -0400 Subject: [PATCH 073/169] chore: don't double import --- modmail/extensions/meta.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modmail/extensions/meta.py b/modmail/extensions/meta.py index f09b95a3..eb5e8650 100644 --- a/modmail/extensions/meta.py +++ b/modmail/extensions/meta.py @@ -1,7 +1,6 @@ import logging from discord.ext import commands -from discord.ext.commands import Context from modmail.bot import ModmailBot from modmail.log import ModmailLogger @@ -21,7 +20,7 @@ def __init__(self, bot: ModmailBot): self.bot = bot @commands.command(name="ping", aliases=("pong",)) - async def ping(self, ctx: Context) -> None: + async def ping(self, ctx: commands.Context) -> None: """Ping the bot to see its latency and state.""" await ctx.send(f"{round(self.bot.latency * 1000)}ms") From 8d22ae1eb4b29ed557c9d986d99b4970235d2d92 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:27:47 -0700 Subject: [PATCH 074/169] Send a message when there are no plugins installed --- modmail/extensions/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 3a990a18..733a4f0e 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -193,7 +193,7 @@ async def list_extensions(self, ctx: Context) -> None: log.debug(f"{ctx.author} requested a list of all {self.type}s. " "Returning a paginated list.") # since we currently don't have a paginator. - await ctx.send("".join(lines)) + await ctx.send("".join(lines) or f"( There are no {self.type}s installed. )") def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" From a24221a46506bf86806b4e37dd7b82ff1271a1a9 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:28:16 -0700 Subject: [PATCH 075/169] Use callback() so the command coroutine is invoked like we want --- modmail/extensions/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 733a4f0e..5ef3ed77 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -306,7 +306,7 @@ async def load_plugin(self, ctx: Context, *plugins: PluginConverter) -> None: If '\*' or '\*\*' is given as the name, all unloaded plugins will be loaded. """ # noqa: W605 - await self.load_extensions(ctx, *plugins) + await self.load_extensions.callback(self, ctx, *plugins) @plugins_group.command(name="unload", aliases=("ul",)) async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: @@ -315,7 +315,7 @@ async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: If '\*' or '\*\*' is given as the name, all loaded plugins will be unloaded. """ # noqa: W605 - await self.unload_extensions(ctx, *plugins) + await self.unload_extensions.callback(self, ctx, *plugins) @plugins_group.command(name="reload", aliases=("r",)) async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: @@ -327,7 +327,7 @@ async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: If '*' is given as the name, all currently loaded extensions will be reloaded. If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. """ - await self.reload_extensions(ctx, *plugins) + await self.reload_extensions.callback(self, ctx, *plugins) @plugins_group.command(name="list", aliases=("all",)) async def list_plugins(self, ctx: Context) -> None: @@ -337,7 +337,7 @@ async def list_plugins(self, ctx: Context) -> None: Grey indicates that the extension is unloaded. Green indicates that the extension is currently loaded. """ - await self.list_extensions(ctx) + await self.list_extensions.callback(self, ctx) # TODO: Implement install/enable/disable/etc From 7126469fc20fdae234c656d224085c6c639bdffc Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:29:18 -0700 Subject: [PATCH 076/169] Add ls alias for listing extensions and cogs --- modmail/extensions/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 5ef3ed77..7b9448e7 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -168,7 +168,7 @@ async def reload_extensions(self, ctx: Context, *extensions: ExtensionConverter) await ctx.send(self.batch_manage(Action.RELOAD, *extensions)) - @extensions_group.command(name="list", aliases=("all",)) + @extensions_group.command(name="list", aliases=("all", "ls")) async def list_extensions(self, ctx: Context) -> None: """ Get a list of all extensions, including their loaded status. @@ -329,7 +329,7 @@ async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: """ await self.reload_extensions.callback(self, ctx, *plugins) - @plugins_group.command(name="list", aliases=("all",)) + @plugins_group.command(name="list", aliases=("all", "ls")) async def list_plugins(self, ctx: Context) -> None: """ Get a list of all extensions, including their loaded status. From b39d541ad8c16a27c9b4e7628307f82b8c80b06a Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:29:28 -0700 Subject: [PATCH 077/169] Nit: dash in comment --- modmail/extensions/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 7b9448e7..42a0fa19 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -343,7 +343,7 @@ async def list_plugins(self, ctx: Context) -> None: # Delete the commands from ExtensionManager before -# discord.py tries to screw up by reregistering them +# discord.py tries to screw up by re-registering them for command in ExtensionManager.__cog_commands__: PluginManager.__cog_commands__.remove(command) From c284a5556cd46a375bb72eede5a3abfbdd407357 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:30:54 -0700 Subject: [PATCH 078/169] Make default extensions properly inherit from ModmailCog --- modmail/extensions/core.py | 4 ++-- modmail/extensions/meta.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 42a0fa19..00d58c14 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -13,7 +13,7 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import ExtMetadata +from modmail.utils.cogs import ExtMetadata, ModmailCog from modmail.utils.extensions import EXTENSIONS, unqualify from modmail.utils.plugin_manager import PLUGINS @@ -88,7 +88,7 @@ class PluginConverter(ExtensionConverter): type = "plugin" -class ExtensionManager(commands.Cog): +class ExtensionManager(ModmailCog): """Extension management base class.""" type = "extension" diff --git a/modmail/extensions/meta.py b/modmail/extensions/meta.py index eb5e8650..867b54c1 100644 --- a/modmail/extensions/meta.py +++ b/modmail/extensions/meta.py @@ -4,7 +4,7 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import BotModes, ExtMetadata +from modmail.utils.cogs import BotModes, ExtMetadata, ModmailCog log: ModmailLogger = logging.getLogger(__name__) @@ -13,7 +13,7 @@ EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) -class Meta(commands.Cog): +class Meta(ModmailCog): """Meta commands to get info about the bot itself.""" def __init__(self, bot: ModmailBot): From 75a6e336e2f1b287837482a156cfe7584c5678a5 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:36:36 -0700 Subject: [PATCH 079/169] Fix remove cog having incorrect typing --- modmail/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/bot.py b/modmail/bot.py index 8817a086..b3b0b23a 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -78,7 +78,7 @@ def add_cog(self, cog: commands.Cog) -> None: super().add_cog(cog) self.logger.info(f"Cog loaded: {cog.qualified_name}") - def remove_cog(self, cog: commands.Cog) -> None: + def remove_cog(self, cog: str) -> None: """ Delegate to super to unregister `cog`. From 7c17d69d02cd195673661e7758e4abfbd9848641 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:36:50 -0700 Subject: [PATCH 080/169] Fix: Remove cog logging should be INFO level --- modmail/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/bot.py b/modmail/bot.py index b3b0b23a..5e65a677 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -85,7 +85,7 @@ def remove_cog(self, cog: str) -> None: This only serves to make the debug log, so that extensions don't have to. """ super().remove_cog(cog) - self.logger.trace(f"Cog unloaded: {cog}") + self.logger.info(f"Cog unloaded: {cog}") async def on_ready(self) -> None: """Send basic login success message.""" From 6af7c6a521f7f9e84adc361c9ab3124ba8183566 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:38:20 -0700 Subject: [PATCH 081/169] Fix add_cog and make it warn if we're loading a non-ModmailCog --- modmail/bot.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 5e65a677..30540bfb 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -69,13 +69,21 @@ def load_plugins(self) -> None: self.logger.debug(f"Loading plugin {plugin}") self.load_extension(plugin) - def add_cog(self, cog: commands.Cog) -> None: + def add_cog(self, cog: commands.Cog, *, override: bool = False) -> None: """ - Delegate to super to register `cog`. + Load a given cog. - This only serves to make the info log, so that extensions don't have to. + Utilizes the default discord.py loader beneath, but also checks so we can warn when we're + loading a non-ModmailCog cog. """ - super().add_cog(cog) + from modmail.utils.cogs import ModmailCog + + if not isinstance(cog, ModmailCog): + self.logger.warning( + f"Cog {cog.name} is not a ModmailCog. All loaded cogs should always be" + f" instances of ModmailCog." + ) + super().add_cog(cog, override=override) self.logger.info(f"Cog loaded: {cog.qualified_name}") def remove_cog(self, cog: str) -> None: From 8e61c38bafa488adbb8b3d42e30b7dd1ba4e280b Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:39:32 -0700 Subject: [PATCH 082/169] Change okhand to thumbsup --- modmail/extensions/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 00d58c14..f4b4f255 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -231,7 +231,7 @@ def batch_manage(self, action: Action, *extensions: str) -> str: if error: failures[extension] = error - emoji = ":x:" if failures else ":ok_hand:" + emoji = ":x:" if failures else ":thumbsup:" msg = f"{emoji} {len(extensions) - len(failures)} / {len(extensions)} {self.type}s {verb}ed." if failures: @@ -265,7 +265,7 @@ def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: error_msg = f"{e.__class__.__name__}: {e}" msg = f":x: Failed to {verb} {self.type} `{ext}`:\n```\n{error_msg}```" else: - msg = f":ok_hand: {self.type.capitalize()} successfully {verb}ed: `{ext}`." + msg = f":thumbsup: {self.type.capitalize()} successfully {verb}ed: `{ext}`." log.debug(error_msg or msg) return msg, error_msg From d763bcd14ed0ae59b02e4bed091349644991004c Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 00:40:09 -0700 Subject: [PATCH 083/169] Add back exts alias for extensions --- modmail/extensions/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index f4b4f255..63119e5d 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -101,7 +101,7 @@ async def get_black_listed_extensions() -> list: """Returns a list of all blacklisted extensions.""" raise NotImplementedError() - @commands.group("ext", aliases=("extensions",), invoke_without_command=True) + @commands.group("ext", aliases=("extensions", "exts"), invoke_without_command=True) async def extensions_group(self, ctx: Context) -> None: """Load, unload, reload, and list loaded extensions.""" await ctx.send_help(ctx.command) From 489015b7c26ad97300aa6bd855b5e9a2e8b35794 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 01:22:14 -0700 Subject: [PATCH 084/169] Change BotModes Metadata calculations to be on the ExtMetadata class --- modmail/utils/cogs.py | 47 ++++++++++++++++++--------------- modmail/utils/extensions.py | 6 ++--- modmail/utils/plugin_manager.py | 7 ++--- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index 8e60cac1..0f259bc6 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from enum import IntEnum, auto +from typing import Any, Set from discord.ext import commands @@ -11,21 +12,6 @@ def _generate_next_value_(name, start, count, last_values) -> int: # noqa: ANN0 return 1 << count -class BotModes(BitwiseAutoEnum): - """ - Valid modes for the bot. - - These values affect logging levels, which extensions are loaded, and so forth. - """ - - production = auto() - develop = auto() - plugin_dev = auto() - - -BOT_MODES = BotModes - - @dataclass() class ExtMetadata: """Ext metadata class to determine if extension should load at runtime depending on bot configuration.""" @@ -40,13 +26,32 @@ class ExtMetadata: # used for loading bot plugins that help with plugin debugging plugin_dev: bool = False + def __int__(self) -> int: + """Calculate the combination of different variables and return the binary combination.""" + return sum(getattr(self, attribute.name, False) * attribute.value for attribute in BotModes) + + def strings(self) -> Set[str]: + """Gets the enabled modes in text form from a given metadata""" + return {attr.name for attr in BotModes if getattr(self, attr.name, False)} -def calc_mode(metadata: ExtMetadata) -> int: - """Calculate the combination of different variables and return the binary combination.""" - mode = getattr(metadata, "production", False) - mode += getattr(metadata, "develop", False) << 1 - mode += getattr(metadata, "plugin_dev", False) << 2 - return mode + @classmethod + def from_any(cls, other: Any) -> "ExtMetadata": + return cls(**{attr.name: getattr(other, attr.name, False) for attr in BotModes}) + + +class BotModes(BitwiseAutoEnum): + """ + Valid modes for the bot. + + These values affect logging levels, which extensions are loaded, and so forth. + """ + + production = auto() + develop = auto() + plugin_dev = auto() + + +BOT_MODES = BotModes class ModmailCog(commands.Cog): diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index e4919214..a85a73c2 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -10,9 +10,9 @@ from modmail import extensions from modmail.config import CONFIG from modmail.log import ModmailLogger -from modmail.utils.cogs import BOT_MODES, calc_mode +from modmail.utils.cogs import BOT_MODES, ExtMetadata -BOT_MODE = calc_mode(CONFIG.dev) +BOT_MODE = int(ExtMetadata.from_any(CONFIG.dev)) log: ModmailLogger = logging.getLogger(__name__) log.trace(f"BOT_MODE value: {BOT_MODE}") log.debug(f"Dev mode status: {bool(BOT_MODE & BOT_MODES.develop)}") @@ -50,7 +50,7 @@ def on_error(name: str) -> NoReturn: ext_metadata = getattr(imported, "EXT_METADATA", None) if ext_metadata is not None: # check if this cog is dev only or plugin dev only - load_cog = bool(calc_mode(ext_metadata) & BOT_MODE) + load_cog = bool(int(ext_metadata) & BOT_MODE) log.trace(f"Load cog {module.name!r}?: {load_cog}") yield module.name, load_cog continue diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py index ed7e8cf9..a429da26 100644 --- a/modmail/utils/plugin_manager.py +++ b/modmail/utils/plugin_manager.py @@ -18,14 +18,15 @@ from modmail import plugins from modmail.config import CONFIG from modmail.log import ModmailLogger -from modmail.utils.cogs import calc_mode +from modmail.utils.cogs import ExtMetadata -BOT_MODE = calc_mode(CONFIG.dev) +BOT_MODE = int(ExtMetadata.from_any(CONFIG.dev)) BASE_PATH = Path(plugins.__file__).parent log: ModmailLogger = logging.getLogger(__name__) log.trace(f"BOT_MODE value: {BOT_MODE}") +log.trace(f"BOT_MODE values: {ExtMetadata.from_any(CONFIG.dev).strings()}") def unqualify(name: str) -> str: @@ -53,7 +54,7 @@ def walk_plugins() -> Iterator[str]: ext_metadata = getattr(imported, "EXT_METADATA", None) if ext_metadata is not None: # check if this plugin is dev only or plugin dev only - load_cog = calc_mode(ext_metadata & BOT_MODE) + load_cog = (ext_metadata & BOT_MODE).to_strings() log.trace(f"Load plugin {imported.__name__!r}?: {load_cog}") yield imported.__name__, load_cog continue From 85880cdbc283c29332c2b4941dcd0ccd9cafec15 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 01:23:01 -0700 Subject: [PATCH 085/169] Rename ExtMetadata -> ModeMetadata --- modmail/extensions/core.py | 4 ++-- modmail/extensions/meta.py | 4 ++-- modmail/utils/cogs.py | 4 ++-- modmail/utils/extensions.py | 4 ++-- modmail/utils/plugin_manager.py | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 63119e5d..bca17cd2 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -13,7 +13,7 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import ExtMetadata, ModmailCog +from modmail.utils.cogs import ModeMetadata, ModmailCog from modmail.utils.extensions import EXTENSIONS, unqualify from modmail.utils.plugin_manager import PLUGINS @@ -22,7 +22,7 @@ BASE_PATH_LEN = __name__.count(".") -EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) +EXT_METADATA = ModeMetadata(production=True, develop=True, plugin_dev=True) class Action(Enum): diff --git a/modmail/extensions/meta.py b/modmail/extensions/meta.py index 867b54c1..66fc76a9 100644 --- a/modmail/extensions/meta.py +++ b/modmail/extensions/meta.py @@ -4,13 +4,13 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import BotModes, ExtMetadata, ModmailCog +from modmail.utils.cogs import BotModes, ModeMetadata, ModmailCog log: ModmailLogger = logging.getLogger(__name__) print(BotModes.plugin_dev) -EXT_METADATA = ExtMetadata(production=True, develop=True, plugin_dev=True) +EXT_METADATA = ModeMetadata(production=True, develop=True, plugin_dev=True) class Meta(ModmailCog): diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index 0f259bc6..1ea5c20c 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -13,7 +13,7 @@ def _generate_next_value_(name, start, count, last_values) -> int: # noqa: ANN0 @dataclass() -class ExtMetadata: +class ModeMetadata: """Ext metadata class to determine if extension should load at runtime depending on bot configuration.""" # prod mode @@ -35,7 +35,7 @@ def strings(self) -> Set[str]: return {attr.name for attr in BotModes if getattr(self, attr.name, False)} @classmethod - def from_any(cls, other: Any) -> "ExtMetadata": + def from_any(cls, other: Any) -> "ModeMetadata": return cls(**{attr.name: getattr(other, attr.name, False) for attr in BotModes}) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index a85a73c2..7150d6e3 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -10,9 +10,9 @@ from modmail import extensions from modmail.config import CONFIG from modmail.log import ModmailLogger -from modmail.utils.cogs import BOT_MODES, ExtMetadata +from modmail.utils.cogs import BOT_MODES, ModeMetadata -BOT_MODE = int(ExtMetadata.from_any(CONFIG.dev)) +BOT_MODE = int(ModeMetadata.from_any(CONFIG.dev)) log: ModmailLogger = logging.getLogger(__name__) log.trace(f"BOT_MODE value: {BOT_MODE}") log.debug(f"Dev mode status: {bool(BOT_MODE & BOT_MODES.develop)}") diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py index a429da26..6064e886 100644 --- a/modmail/utils/plugin_manager.py +++ b/modmail/utils/plugin_manager.py @@ -18,15 +18,15 @@ from modmail import plugins from modmail.config import CONFIG from modmail.log import ModmailLogger -from modmail.utils.cogs import ExtMetadata +from modmail.utils.cogs import ModeMetadata -BOT_MODE = int(ExtMetadata.from_any(CONFIG.dev)) +BOT_MODE = int(ModeMetadata.from_any(CONFIG.dev)) BASE_PATH = Path(plugins.__file__).parent log: ModmailLogger = logging.getLogger(__name__) log.trace(f"BOT_MODE value: {BOT_MODE}") -log.trace(f"BOT_MODE values: {ExtMetadata.from_any(CONFIG.dev).strings()}") +log.trace(f"BOT_MODE values: {ModeMetadata.from_any(CONFIG.dev).strings()}") def unqualify(name: str) -> str: From ff1a100bac73a14607c63acb03989747d65a397a Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 01:25:31 -0700 Subject: [PATCH 086/169] Remove outdated comment --- modmail/extensions/core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index bca17cd2..0254dcd3 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -289,8 +289,6 @@ class PluginManager(ExtensionManager): type = "plugin" def __init__(self, bot: ModmailBot) -> None: - # We don't use super because discord.py uses superclasses for something - # and we just want regular inheritance super().__init__(bot) self.all_extensions = PLUGINS From 97424d3ec7c1909223c7f90400b406a85d2023e2 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sat, 7 Aug 2021 04:27:22 -0400 Subject: [PATCH 087/169] touch up core utils, dry code --- modmail/utils/extensions.py | 6 +++--- modmail/utils/plugin_manager.py | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index e4919214..464b5eed 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -14,10 +14,13 @@ BOT_MODE = calc_mode(CONFIG.dev) log: ModmailLogger = logging.getLogger(__name__) + log.trace(f"BOT_MODE value: {BOT_MODE}") log.debug(f"Dev mode status: {bool(BOT_MODE & BOT_MODES.develop)}") log.debug(f"Plugin dev mode status: {bool(BOT_MODE & BOT_MODES.plugin_dev)}") +EXTENSIONS = dict() + def unqualify(name: str) -> str: """Return an unqualified name given a qualified module/package `name`.""" @@ -59,6 +62,3 @@ def on_error(name: str) -> NoReturn: # Presume Production Mode yield module.name, True - - -EXTENSIONS = dict() diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugin_manager.py index ed7e8cf9..451a0b92 100644 --- a/modmail/utils/plugin_manager.py +++ b/modmail/utils/plugin_manager.py @@ -19,18 +19,15 @@ from modmail.config import CONFIG from modmail.log import ModmailLogger from modmail.utils.cogs import calc_mode +from modmail.utils.extensions import unqualify BOT_MODE = calc_mode(CONFIG.dev) BASE_PATH = Path(plugins.__file__).parent - log: ModmailLogger = logging.getLogger(__name__) log.trace(f"BOT_MODE value: {BOT_MODE}") - -def unqualify(name: str) -> str: - """Return an unqualified name given a qualified module/package `name`.""" - return name.rsplit(".", maxsplit=1)[-1] +PLUGINS = dict() def walk_plugins() -> Iterator[str]: @@ -42,6 +39,13 @@ def walk_plugins() -> Iterator[str]: name = "modmail.plugins." + name log.trace("Relative path: {0}".format(name)) + if unqualify(name.split(".")[-1]).startswith("_"): + # Ignore module/package names starting with an underscore. + continue + + # load the plugins using importlib + # this needs to be done like this, due to the fact that + # its possible a plugin will not have an __init__.py file spec = importlib.util.spec_from_file_location(name, path) imported = importlib.util.module_from_spec(spec) spec.loader.exec_module(imported) @@ -58,11 +62,8 @@ def walk_plugins() -> Iterator[str]: yield imported.__name__, load_cog continue - log.notice( + log.info( f"Plugin {imported.__name__!r} is missing a EXT_METADATA variable. Assuming its a normal plugin." ) yield imported.__name__, True - - -PLUGINS = dict() From 3ae50ea1f2c111148c5f891eb4853f070c759e15 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 01:32:42 -0700 Subject: [PATCH 088/169] Nit: reword comment to be cleaner --- modmail/extensions/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/extensions/core.py b/modmail/extensions/core.py index 0254dcd3..eb480c3f 100644 --- a/modmail/extensions/core.py +++ b/modmail/extensions/core.py @@ -340,8 +340,8 @@ async def list_plugins(self, ctx: Context) -> None: # TODO: Implement install/enable/disable/etc -# Delete the commands from ExtensionManager before -# discord.py tries to screw up by re-registering them +# Delete the commands from ExtensionManager that PluginManager has inherited +# before discord.py tries to re-register them for command in ExtensionManager.__cog_commands__: PluginManager.__cog_commands__.remove(command) From c53f8e9f90156d25622d8f325cd121c6e89bbd1f Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 01:35:35 -0700 Subject: [PATCH 089/169] Add/cleanup missing docstring in utils/cogs --- modmail/utils/cogs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index 1ea5c20c..5a97b211 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -31,11 +31,12 @@ def __int__(self) -> int: return sum(getattr(self, attribute.name, False) * attribute.value for attribute in BotModes) def strings(self) -> Set[str]: - """Gets the enabled modes in text form from a given metadata""" + """Gets the enabled modes in text form from a given metadata.""" return {attr.name for attr in BotModes if getattr(self, attr.name, False)} @classmethod def from_any(cls, other: Any) -> "ModeMetadata": + """Generate modes from an arbitrary class (such as a configuration class).""" return cls(**{attr.name: getattr(other, attr.name, False) for attr in BotModes}) From 36df7b415ba8e8bc5c61188121cdcd9e21525ee6 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 01:58:50 -0700 Subject: [PATCH 090/169] Rename extensions to extension_manager to help split ExtensionManager and PluginManager into separate modules --- modmail/extensions/{core.py => extension_manager.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modmail/extensions/{core.py => extension_manager.py} (100%) diff --git a/modmail/extensions/core.py b/modmail/extensions/extension_manager.py similarity index 100% rename from modmail/extensions/core.py rename to modmail/extensions/extension_manager.py From 494d0399c33772257a3b05e4f367f8f52abc7071 Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 01:59:15 -0700 Subject: [PATCH 091/169] Split ExtensionManager and PluginManager into separate modules ty pycharm, you're the best <3 --- modmail/extensions/extension_manager.py | 79 +--------------------- modmail/extensions/plugin_manager.py | 89 +++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 78 deletions(-) create mode 100644 modmail/extensions/plugin_manager.py diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index eb480c3f..156f3ef0 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -15,7 +15,6 @@ from modmail.log import ModmailLogger from modmail.utils.cogs import ModeMetadata, ModmailCog from modmail.utils.extensions import EXTENSIONS, unqualify -from modmail.utils.plugin_manager import PLUGINS log: ModmailLogger = logging.getLogger(__name__) @@ -77,17 +76,6 @@ async def convert(self, ctx: Context, argument: str) -> str: return matches[0] -class PluginConverter(ExtensionConverter): - """ - Fully qualify the name of a plugin and ensure it exists. - - The * and ** values bypass this when used with the reload command. - """ - - source_list = PLUGINS - type = "plugin" - - class ExtensionManager(ModmailCog): """Extension management base class.""" @@ -283,71 +271,6 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: error.handled = True -class PluginManager(ExtensionManager): - """Plugin management commands.""" - - type = "plugin" - - def __init__(self, bot: ModmailBot) -> None: - super().__init__(bot) - self.all_extensions = PLUGINS - - @commands.group("plugins", aliases=("plug", "plugs"), invoke_without_command=True) - async def plugins_group(self, ctx: Context) -> None: - """Install, uninstall, disable, update, and enable installed plugins.""" - await ctx.send_help(ctx.command) - - @plugins_group.command(name="load", aliases=("l",)) - async def load_plugin(self, ctx: Context, *plugins: PluginConverter) -> None: - """ - Load plugins given their fully qualified or unqualified names. - - If '\*' or '\*\*' is given as the name, all unloaded plugins will be loaded. - """ # noqa: W605 - await self.load_extensions.callback(self, ctx, *plugins) - - @plugins_group.command(name="unload", aliases=("ul",)) - async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: - """ - Unload currently loaded plugins given their fully qualified or unqualified names. - - If '\*' or '\*\*' is given as the name, all loaded plugins will be unloaded. - """ # noqa: W605 - await self.unload_extensions.callback(self, ctx, *plugins) - - @plugins_group.command(name="reload", aliases=("r",)) - async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: - """ - Reload extensions given their fully qualified or unqualified names. - - If an extension fails to be reloaded, it will be rolled-back to the prior working state. - - If '*' is given as the name, all currently loaded extensions will be reloaded. - If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. - """ - await self.reload_extensions.callback(self, ctx, *plugins) - - @plugins_group.command(name="list", aliases=("all", "ls")) - async def list_plugins(self, ctx: Context) -> None: - """ - Get a list of all extensions, including their loaded status. - - Grey indicates that the extension is unloaded. - Green indicates that the extension is currently loaded. - """ - await self.list_extensions.callback(self, ctx) - - # TODO: Implement install/enable/disable/etc - - -# Delete the commands from ExtensionManager that PluginManager has inherited -# before discord.py tries to re-register them -for command in ExtensionManager.__cog_commands__: - PluginManager.__cog_commands__.remove(command) - - def setup(bot: ModmailBot) -> None: - """Load the Plugins manager cog.""" - # PluginManager includes the ExtensionManager + """Load the Extension Manager cog.""" bot.add_cog(ExtensionManager(bot)) - bot.add_cog(PluginManager(bot)) diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py new file mode 100644 index 00000000..c8fdc24a --- /dev/null +++ b/modmail/extensions/plugin_manager.py @@ -0,0 +1,89 @@ +from discord.ext import commands +from discord.ext.commands import Context + +from modmail.bot import ModmailBot +from modmail.extensions.extension_manager import ExtensionConverter, ExtensionManager +from modmail.utils.cogs import ModeMetadata +from modmail.utils.plugin_manager import PLUGINS + +EXT_METADATA = ModeMetadata(production=True, develop=True, plugin_dev=True) + + +class PluginConverter(ExtensionConverter): + """ + Fully qualify the name of a plugin and ensure it exists. + + The * and ** values bypass this when used with the reload command. + """ + + source_list = PLUGINS + type = "plugin" + + +class PluginManager(ExtensionManager): + """Plugin management commands.""" + + type = "plugin" + + def __init__(self, bot: ModmailBot) -> None: + super().__init__(bot) + self.all_extensions = PLUGINS + + @commands.group("plugins", aliases=("plug", "plugs"), invoke_without_command=True) + async def plugins_group(self, ctx: Context) -> None: + """Install, uninstall, disable, update, and enable installed plugins.""" + await ctx.send_help(ctx.command) + + @plugins_group.command(name="load", aliases=("l",)) + async def load_plugin(self, ctx: Context, *plugins: PluginConverter) -> None: + """ + Load plugins given their fully qualified or unqualified names. + + If '\*' or '\*\*' is given as the name, all unloaded plugins will be loaded. + """ # noqa: W605 + await self.load_extensions.callback(self, ctx, *plugins) + + @plugins_group.command(name="unload", aliases=("ul",)) + async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: + """ + Unload currently loaded plugins given their fully qualified or unqualified names. + + If '\*' or '\*\*' is given as the name, all loaded plugins will be unloaded. + """ # noqa: W605 + await self.unload_extensions.callback(self, ctx, *plugins) + + @plugins_group.command(name="reload", aliases=("r",)) + async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: + """ + Reload extensions given their fully qualified or unqualified names. + + If an extension fails to be reloaded, it will be rolled-back to the prior working state. + + If '*' is given as the name, all currently loaded extensions will be reloaded. + If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. + """ + await self.reload_extensions.callback(self, ctx, *plugins) + + @plugins_group.command(name="list", aliases=("all", "ls")) + async def list_plugins(self, ctx: Context) -> None: + """ + Get a list of all extensions, including their loaded status. + + Grey indicates that the extension is unloaded. + Green indicates that the extension is currently loaded. + """ + await self.list_extensions.callback(self, ctx) + + # TODO: Implement install/enable/disable/etc + + +# Delete the commands from ExtensionManager that PluginManager has inherited +# before discord.py tries to re-register them +for command in ExtensionManager.__cog_commands__: + PluginManager.__cog_commands__.remove(command) + + +def setup(bot: ModmailBot) -> None: + """Load the Plugins Manager cog.""" + # PluginManager includes the ExtensionManager + bot.add_cog(PluginManager(bot)) From d137c14eb67d5b227812ccc012156c94cd5c516b Mon Sep 17 00:00:00 2001 From: Bast Date: Sat, 7 Aug 2021 02:01:40 -0700 Subject: [PATCH 092/169] Rename second utils.plugin_manager to utils.plugins --- modmail/bot.py | 2 +- modmail/extensions/plugin_manager.py | 2 +- modmail/utils/{plugin_manager.py => plugins.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename modmail/utils/{plugin_manager.py => plugins.py} (100%) diff --git a/modmail/bot.py b/modmail/bot.py index 30540bfb..08cdb9d2 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -61,7 +61,7 @@ def load_extensions(self) -> None: def load_plugins(self) -> None: """Load all enabled plugins.""" - from modmail.utils.plugin_manager import PLUGINS, walk_plugins + from modmail.utils.plugins import PLUGINS, walk_plugins PLUGINS.update(walk_plugins()) for plugin, should_load in PLUGINS.items(): diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index c8fdc24a..387955e9 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -4,7 +4,7 @@ from modmail.bot import ModmailBot from modmail.extensions.extension_manager import ExtensionConverter, ExtensionManager from modmail.utils.cogs import ModeMetadata -from modmail.utils.plugin_manager import PLUGINS +from modmail.utils.plugins import PLUGINS EXT_METADATA = ModeMetadata(production=True, develop=True, plugin_dev=True) diff --git a/modmail/utils/plugin_manager.py b/modmail/utils/plugins.py similarity index 100% rename from modmail/utils/plugin_manager.py rename to modmail/utils/plugins.py From 87d6734e097fbb31a99ad057cf4fbaede3984d75 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sun, 8 Aug 2021 10:50:32 -0400 Subject: [PATCH 093/169] chore: escape in docstrings for discord --- modmail/extensions/extension_manager.py | 6 +++--- modmail/extensions/plugin_manager.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 156f3ef0..9bd5f81d 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -141,9 +141,9 @@ async def reload_extensions(self, ctx: Context, *extensions: ExtensionConverter) If an extension fails to be reloaded, it will be rolled-back to the prior working state. - If '*' is given as the name, all currently loaded extensions will be reloaded. - If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. - """ + If '\*' is given as the name, all currently loaded extensions will be reloaded. + If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. + """ # noqa: W605 if not extensions: await ctx.send_help(ctx.command) return diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 387955e9..0e4d5abb 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -13,8 +13,8 @@ class PluginConverter(ExtensionConverter): """ Fully qualify the name of a plugin and ensure it exists. - The * and ** values bypass this when used with the reload command. - """ + The \* and \*\* values bypass this when used with the reload command. + """ # noqa: W605 source_list = PLUGINS type = "plugin" @@ -59,9 +59,9 @@ async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: If an extension fails to be reloaded, it will be rolled-back to the prior working state. - If '*' is given as the name, all currently loaded extensions will be reloaded. - If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. - """ + If '\*' is given as the name, all currently loaded extensions will be reloaded. + If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. + """ # noqa: W605 await self.reload_extensions.callback(self, ctx, *plugins) @plugins_group.command(name="list", aliases=("all", "ls")) From e5dec18fae17e32e27f4c4ef875f447ba524ed9e Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sun, 8 Aug 2021 12:39:21 -0400 Subject: [PATCH 094/169] chore: modify metadata to allow for other metadata --- modmail/config-default.toml | 3 ++ modmail/config.py | 16 +++++++-- modmail/extensions/extension_manager.py | 4 +-- modmail/extensions/meta.py | 6 ++-- modmail/extensions/plugin_manager.py | 4 +-- modmail/utils/cogs.py | 45 +++++++------------------ modmail/utils/extensions.py | 31 +++++++++++++---- modmail/utils/plugins.py | 12 +++---- 8 files changed, 64 insertions(+), 57 deletions(-) diff --git a/modmail/config-default.toml b/modmail/config-default.toml index 7885da20..65525306 100644 --- a/modmail/config-default.toml +++ b/modmail/config-default.toml @@ -27,6 +27,9 @@ update_channel_id = "0" [dev] log_level = 25 # "NOTICE" +[dev.mode] +production = true + [emoji] # sent_emoji = "white_heavy_check_mark" # blocked_emoji = "\\N{NO ENTRY SIGN}" diff --git a/modmail/config.py b/modmail/config.py index 5902ce75..5651a7eb 100644 --- a/modmail/config.py +++ b/modmail/config.py @@ -145,6 +145,18 @@ class ChannelConfig(BaseSettings): update_channel: str = None +class BotMode(BaseSettings): + """ + Bot mode. + + Used to determine when the bot will run. + """ + + production: bool = True + plugin_dev: bool = False + develop: bool = False + + class DevConfig(BaseSettings): """ Developer specific configuration. @@ -152,9 +164,7 @@ class DevConfig(BaseSettings): """ log_level: conint(ge=0, le=50) = getattr(logging, "NOTICE", 25) - production: bool = True - develop: bool = False - plugin_dev: bool = False + mode: BotMode class EmojiConfig(BaseSettings): diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 9bd5f81d..92d78f70 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -13,7 +13,7 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import ModeMetadata, ModmailCog +from modmail.utils.cogs import BotModes, ExtMetadata, ModmailCog from modmail.utils.extensions import EXTENSIONS, unqualify log: ModmailLogger = logging.getLogger(__name__) @@ -21,7 +21,7 @@ BASE_PATH_LEN = __name__.count(".") -EXT_METADATA = ModeMetadata(production=True, develop=True, plugin_dev=True) +EXT_METADATA = ExtMetadata(load_if_mode=BotModes.DEVELOP) class Action(Enum): diff --git a/modmail/extensions/meta.py b/modmail/extensions/meta.py index 66fc76a9..d90b0146 100644 --- a/modmail/extensions/meta.py +++ b/modmail/extensions/meta.py @@ -4,13 +4,11 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import BotModes, ModeMetadata, ModmailCog +from modmail.utils.cogs import ExtMetadata, ModmailCog log: ModmailLogger = logging.getLogger(__name__) -print(BotModes.plugin_dev) - -EXT_METADATA = ModeMetadata(production=True, develop=True, plugin_dev=True) +EXT_METADATA = ExtMetadata() class Meta(ModmailCog): diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 0e4d5abb..744cab14 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -3,10 +3,10 @@ from modmail.bot import ModmailBot from modmail.extensions.extension_manager import ExtensionConverter, ExtensionManager -from modmail.utils.cogs import ModeMetadata +from modmail.utils.cogs import BotModes, ExtMetadata from modmail.utils.plugins import PLUGINS -EXT_METADATA = ModeMetadata(production=True, develop=True, plugin_dev=True) +EXT_METADATA = ExtMetadata(load_if_mode=BotModes.PRODUCTION) class PluginConverter(ExtensionConverter): diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index 5a97b211..cea6234c 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from enum import IntEnum, auto -from typing import Any, Set from discord.ext import commands @@ -12,34 +11,6 @@ def _generate_next_value_(name, start, count, last_values) -> int: # noqa: ANN0 return 1 << count -@dataclass() -class ModeMetadata: - """Ext metadata class to determine if extension should load at runtime depending on bot configuration.""" - - # prod mode - # set this to true if the cog should always load - production: bool = False - # load if bot is in development mode - # development mode is when the bot has its metacogs loaded, like the eval and extension cogs - develop: bool = False - # plugin development mode - # used for loading bot plugins that help with plugin debugging - plugin_dev: bool = False - - def __int__(self) -> int: - """Calculate the combination of different variables and return the binary combination.""" - return sum(getattr(self, attribute.name, False) * attribute.value for attribute in BotModes) - - def strings(self) -> Set[str]: - """Gets the enabled modes in text form from a given metadata.""" - return {attr.name for attr in BotModes if getattr(self, attr.name, False)} - - @classmethod - def from_any(cls, other: Any) -> "ModeMetadata": - """Generate modes from an arbitrary class (such as a configuration class).""" - return cls(**{attr.name: getattr(other, attr.name, False) for attr in BotModes}) - - class BotModes(BitwiseAutoEnum): """ Valid modes for the bot. @@ -47,14 +18,24 @@ class BotModes(BitwiseAutoEnum): These values affect logging levels, which extensions are loaded, and so forth. """ - production = auto() - develop = auto() - plugin_dev = auto() + PRODUCTION = auto() + DEVELOP = auto() + PLUGIN_DEV = auto() BOT_MODES = BotModes +@dataclass() +class ExtMetadata: + """Ext metadata class to determine if extension should load at runtime depending on bot configuration.""" + + load_if_mode: int = BotModes.PRODUCTION + + def __int__(self, load_if_mode: int = BotModes.PRODUCTION) -> int: + self.load_if_mode = load_if_mode + + class ModmailCog(commands.Cog): """ The base class that all cogs must inherit from. diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index dc6332a1..17432752 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -10,18 +10,37 @@ from modmail import extensions from modmail.config import CONFIG from modmail.log import ModmailLogger -from modmail.utils.cogs import BOT_MODES, ModeMetadata +from modmail.utils.cogs import BOT_MODES, BotModes, ExtMetadata -BOT_MODE = int(ModeMetadata.from_any(CONFIG.dev)) log: ModmailLogger = logging.getLogger(__name__) -log.trace(f"BOT_MODE value: {BOT_MODE}") -log.debug(f"Dev mode status: {bool(BOT_MODE & BOT_MODES.develop)}") -log.debug(f"Plugin dev mode status: {bool(BOT_MODE & BOT_MODES.plugin_dev)}") +EXT_METADATA = ExtMetadata + EXTENSIONS = dict() +def determine_bot_mode() -> int: + """ + Figure out the bot mode from the configuration system. + + The configuration system uses true/false values, so we need to turn them into an integer for bitwise. + """ + bot_mode = 0 + for mode in BotModes: + if getattr(CONFIG.dev.mode, str(mode).split(".")[-1].lower(), True): + bot_mode += mode.value + return bot_mode + + +BOT_MODE = determine_bot_mode() + + +log.trace(f"BOT_MODE value: {BOT_MODE}") +log.debug(f"Dev mode status: {bool(BOT_MODE & BOT_MODES.DEVELOP)}") +log.debug(f"Plugin dev mode status: {bool(BOT_MODE & BOT_MODES.PLUGIN_DEV)}") + + def unqualify(name: str) -> str: """Return an unqualified name given a qualified module/package `name`.""" return name.rsplit(".", maxsplit=1)[-1] @@ -53,7 +72,7 @@ def on_error(name: str) -> NoReturn: ext_metadata = getattr(imported, "EXT_METADATA", None) if ext_metadata is not None: # check if this cog is dev only or plugin dev only - load_cog = bool(int(ext_metadata) & BOT_MODE) + load_cog = bool(int(ext_metadata.load_if_mode) & BOT_MODE) log.trace(f"Load cog {module.name!r}?: {load_cog}") yield module.name, load_cog continue diff --git a/modmail/utils/plugins.py b/modmail/utils/plugins.py index bca05758..83af6296 100644 --- a/modmail/utils/plugins.py +++ b/modmail/utils/plugins.py @@ -16,17 +16,13 @@ from typing import Iterator from modmail import plugins -from modmail.config import CONFIG from modmail.log import ModmailLogger -from modmail.utils.cogs import ModeMetadata -from modmail.utils.extensions import unqualify +from modmail.utils.cogs import ExtMetadata +from modmail.utils.extensions import BOT_MODE, unqualify -BOT_MODE = int(ModeMetadata.from_any(CONFIG.dev)) BASE_PATH = Path(plugins.__file__).parent log: ModmailLogger = logging.getLogger(__name__) -log.trace(f"BOT_MODE value: {BOT_MODE}") -log.trace(f"BOT_MODE values: {ModeMetadata.from_any(CONFIG.dev).strings()}") PLUGINS = dict() @@ -55,10 +51,10 @@ def walk_plugins() -> Iterator[str]: # If it lacks a setup function, it's not a plugin. This is enforced by dpy. continue - ext_metadata = getattr(imported, "EXT_METADATA", None) + ext_metadata: ExtMetadata = getattr(imported, "EXT_METADATA", None) if ext_metadata is not None: # check if this plugin is dev only or plugin dev only - load_cog = (ext_metadata & BOT_MODE).to_strings() + load_cog = (ext_metadata.load_if_mode & BOT_MODE).to_strings() log.trace(f"Load plugin {imported.__name__!r}?: {load_cog}") yield imported.__name__, load_cog continue From fcb4bb0fbedba386daa8662e0be0d765f935526a Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sun, 8 Aug 2021 12:50:44 -0400 Subject: [PATCH 095/169] allow server admins to run plugin commands too --- modmail/extensions/plugin_manager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 744cab14..647af8ed 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -76,6 +76,11 @@ async def list_plugins(self, ctx: Context) -> None: # TODO: Implement install/enable/disable/etc + # This cannot be static (must have a __func__ attribute). + async def cog_check(self, ctx: Context) -> bool: + """Only allow server admins and bot owners to invoke the commands in this cog.""" + return ctx.author.guild_permissions.administrator or await self.bot.is_owner(ctx.author) + # Delete the commands from ExtensionManager that PluginManager has inherited # before discord.py tries to re-register them From 54b8e1e70afaabb4390565cf86d20c3c0702400a Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sun, 8 Aug 2021 13:00:20 -0400 Subject: [PATCH 096/169] get rid of `_` in displayed cog names --- modmail/extensions/extension_manager.py | 2 +- modmail/extensions/plugin_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 92d78f70..dbf1370e 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -76,7 +76,7 @@ async def convert(self, ctx: Context, argument: str) -> str: return matches[0] -class ExtensionManager(ModmailCog): +class ExtensionManager(ModmailCog, name="Extension Manager"): """Extension management base class.""" type = "extension" diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 647af8ed..d4002abe 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -20,7 +20,7 @@ class PluginConverter(ExtensionConverter): type = "plugin" -class PluginManager(ExtensionManager): +class PluginManager(ExtensionManager, name="Plugin Manager"): """Plugin management commands.""" type = "plugin" From 47273c3d8774163b4d62fe2e84c4ba3ddd46f378 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 00:59:38 -0400 Subject: [PATCH 097/169] tools: reorder dependencies and comment them --- pyproject.toml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3698056b..5c3faee4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,9 +27,13 @@ python-dotenv = "~=0.17.1" toml = "^0.10.2" [tool.poetry.dev-dependencies] +# always needed +pre-commit = "~=2.1" +taskipy = "^1.6.0" + +# linting, needed if intending to make a commit +# pre-commit requires flake8 black = "^21.7b0" -codecov = "^2.1.11" -coverage = { extras = ["toml"], version = "^5.5" } flake8 = "~=3.8" flake8-annotations = "~=2.3" flake8-bandit = "^2.1.2" @@ -41,14 +45,17 @@ flake8-tidy-imports = "~=4.1" flake8-todo = "~=0.7" isort = "^5.9.2" pep8-naming = "~=0.11" -pre-commit = "~=2.1" + +# testing +codecov = "^2.1.11" +coverage = { extras = ["toml"], version = "^5.5" } pytest = "^6.2.4" pytest-asyncio = "^0.15.1" pytest-cov = "^2.12.1" pytest-dependency = "^0.5.1" pytest-docs = "^0.1.0" pytest-xdist = { version = "^2.3.0", extras = ["psutil"] } -taskipy = "^1.6.0" + [build-system] requires = ["poetry-core>=1.0.0"] From 923e659bb3584ec6d8ff8988b29c6938c1329784 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 01:00:17 -0400 Subject: [PATCH 098/169] docs: add CONTRIBUTING.md --- CONTRIBUTING.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..bfab4ceb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing to Modmail + +Welcome to Modmail, and thank you for your interest in contributing! + +## Note +We recommend checking open issues for something you would like to work on. If something is interesting, leave a comment and wait for a person to assign you, otherwise someone else may already be working on the same thing. + +## Installation + +To get started, you need: + +- git +- python 3.8 or higher +- poetry + +These are the bare minimum requirements, as poetry is used to install everything needed by our project. You can find installation instructions on [poetry's website][poetry-install]. + +If you have write access to this repo, you may make a new branch and push changes to it here. Otherwise, make a [fork][], and clone it locally to push your changes to. Be sure to push your changes to a new branch. + + +To install all dependencies, run the following command. +```sh +poetry install +``` +This will create a venv for our project, and install everything to the venv. + + +Next, install our pre-commit hook, which will help ensure all commits follow our coding guidelines. + +```sh +poetry run task precommit +``` + +This installs [pre-commit][] to git's hooks, and all of the tools too. + +## Testing + +We use pytest for our testing framework, and to run our tests. + +If you are adding features, we expect tests to be written, and passing. + +To run the entire test suite, use +```sh +poetry run task test +``` +If you would like to run just a specific file's test, use +```sh +poetry run task test {file} +``` + +## Run the bot + +To run the bot, use: +```sh +poetry run task run +``` + +## Tasks + +We use [taskipy][] to run a bunch of our common commands. These are subject to change, but you can always get an up to date list by running the following: +```sh +poetry run task --list +``` + +## A note on Poetry + +All of the commands in this page use `poetry run` for the sake of clarity. However, it is possible to use **`poetry shell`** to enter the venv and therefore not require `poetry run` before every command. + +------- + +## Submit Changes + +To submit your changes, go to the [pulls][] page, select your branch, and create a new pull request pointed towards our repository. +We recommend creating a pull request even while it is in progress, so we can see it as it goes on. + +[fork]: https://github.com/discord-modmail/modmail/fork +[poetry-install]: https://python-poetry.org/docs#installation +[pre-commit]: https://pre-commit.com/ +[pulls]: https://github.com/discord-modmail/modmail/pulls +[taskipy]: https://pypi.org/project/taskipy/ From 6d1401e9ffa291d363230bed11d9583319dea4b6 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 01:32:37 -0400 Subject: [PATCH 099/169] chore: use allowed mentions on errors --- modmail/extensions/extension_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index dbf1370e..8706f33a 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -7,7 +7,7 @@ from collections import defaultdict from enum import Enum -from discord import Colour, Embed +from discord import AllowedMentions, Colour, Embed from discord.ext import commands from discord.ext.commands import Context @@ -267,7 +267,7 @@ async def cog_check(self, ctx: Context) -> bool: async def cog_command_error(self, ctx: Context, error: Exception) -> None: """Handle BadArgument errors locally to prevent the help command from showing.""" if isinstance(error, commands.BadArgument): - await ctx.send(str(error)) + await ctx.send(str(error), allowed_mentions=AllowedMentions.none()) error.handled = True From 2e31fe465883b5b152c56c67d4f341d6f870b9e6 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 01:38:59 -0400 Subject: [PATCH 100/169] chore: get rid of circular import protection now that its not a problem, the circular import protection is not needed --- modmail/utils/extensions.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 17432752..54c8639b 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -57,12 +57,6 @@ def on_error(name: str) -> NoReturn: # Ignore module/package names starting with an underscore. continue - if module.name.endswith("utils.extensions"): - # due to circular imports, the utils.extensions cog is not able to utilize the cog metadata class - # it is hardcoded here as a dev cog in order to prevent it from causing bugs - yield module.name, BOT_MODES.develop & BOT_MODE - continue - imported = importlib.import_module(module.name) if module.ispkg: if not inspect.isfunction(getattr(imported, "setup", None)): From 86f0a5e5db77bf8b364960ae712bcec8ab669eb1 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 02:29:48 -0400 Subject: [PATCH 101/169] fix: unload ext works again, blacklist is now in metadata --- modmail/bot.py | 12 ++++++++---- modmail/extensions/extension_manager.py | 10 +++++----- modmail/extensions/plugin_manager.py | 10 ++++++++++ modmail/utils/cogs.py | 5 ++++- modmail/utils/extensions.py | 14 ++++++++------ 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 08cdb9d2..eb22d53f 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -6,6 +6,7 @@ from discord.ext import commands from modmail.config import CONFIG, INTERNAL +from modmail.utils.extensions import EXTENSIONS, NO_UNLOAD, walk_extensions # noqa: F401 class ModmailBot(commands.Bot): @@ -49,11 +50,14 @@ async def close(self) -> None: await super().close() def load_extensions(self) -> None: - """Load all enabled extensions.""" - # Must be done here to avoid a circular import. - from modmail.utils.extensions import EXTENSIONS, walk_extensions - + """Load all enabled extensions.""" # noqa: F811 EXTENSIONS.update(walk_extensions()) + + # set up no_unload global too + for ext, value in EXTENSIONS.items(): + if value[1]: + NO_UNLOAD.append(ext) + for extension, should_load in EXTENSIONS.items(): if should_load: self.logger.debug(f"Loading extension {extension}") diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 8706f33a..b06e98b4 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -14,14 +14,14 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger from modmail.utils.cogs import BotModes, ExtMetadata, ModmailCog -from modmail.utils.extensions import EXTENSIONS, unqualify +from modmail.utils.extensions import EXTENSIONS, NO_UNLOAD, unqualify log: ModmailLogger = logging.getLogger(__name__) BASE_PATH_LEN = __name__.count(".") -EXT_METADATA = ExtMetadata(load_if_mode=BotModes.DEVELOP) +EXT_METADATA = ExtMetadata(load_if_mode=BotModes.DEVELOP, no_unload=True) class Action(Enum): @@ -85,9 +85,9 @@ def __init__(self, bot: ModmailBot): self.bot = bot self.all_extensions = EXTENSIONS - async def get_black_listed_extensions() -> list: - """Returns a list of all blacklisted extensions.""" - raise NotImplementedError() + async def get_black_listed_extensions(self) -> list: + """Returns a list of all unload blacklisted extensions.""" + return NO_UNLOAD @commands.group("ext", aliases=("extensions", "exts"), invoke_without_command=True) async def extensions_group(self, ctx: Context) -> None: diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index d4002abe..9c5cf4f1 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -18,6 +18,7 @@ class PluginConverter(ExtensionConverter): source_list = PLUGINS type = "plugin" + NO_UNLOAD = None class PluginManager(ExtensionManager, name="Plugin Manager"): @@ -29,6 +30,15 @@ def __init__(self, bot: ModmailBot) -> None: super().__init__(bot) self.all_extensions = PLUGINS + async def get_black_listed_extensions(self) -> list: + """ + Returns a list of all unload blacklisted plugins. + + This method exists to override the one in extensions manager, + due to the fact that blacklisting plugins is not supported. + """ + return [] + @commands.group("plugins", aliases=("plug", "plugs"), invoke_without_command=True) async def plugins_group(self, ctx: Context) -> None: """Install, uninstall, disable, update, and enable installed plugins.""" diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index cea6234c..b0003851 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -31,9 +31,12 @@ class ExtMetadata: """Ext metadata class to determine if extension should load at runtime depending on bot configuration.""" load_if_mode: int = BotModes.PRODUCTION + # this is to determine if the cog is allowed to be unloaded. + no_unload: bool = False - def __int__(self, load_if_mode: int = BotModes.PRODUCTION) -> int: + def __init__(self, load_if_mode: int = BotModes.PRODUCTION, no_unload: bool = False) -> "ExtMetadata": self.load_if_mode = load_if_mode + self.no_unload = no_unload class ModmailCog(commands.Cog): diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 54c8639b..25eb0c82 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -5,7 +5,7 @@ import inspect import logging import pkgutil -from typing import Iterator, NoReturn +from typing import Iterator, NoReturn, Tuple from modmail import extensions from modmail.config import CONFIG @@ -18,6 +18,7 @@ EXTENSIONS = dict() +NO_UNLOAD = list() def determine_bot_mode() -> int: @@ -46,7 +47,7 @@ def unqualify(name: str) -> str: return name.rsplit(".", maxsplit=1)[-1] -def walk_extensions() -> Iterator[str]: +def walk_extensions() -> Iterator[Tuple]: """Yield extension names from the modmail.exts subpackage.""" def on_error(name: str) -> NoReturn: @@ -63,15 +64,16 @@ def on_error(name: str) -> NoReturn: # If it lacks a setup function, it's not an extension. continue - ext_metadata = getattr(imported, "EXT_METADATA", None) + ext_metadata: "ExtMetadata" = getattr(imported, "EXT_METADATA", None) if ext_metadata is not None: # check if this cog is dev only or plugin dev only load_cog = bool(int(ext_metadata.load_if_mode) & BOT_MODE) log.trace(f"Load cog {module.name!r}?: {load_cog}") - yield module.name, load_cog + no_unload = ext_metadata.no_unload + yield module.name, (load_cog, no_unload) continue log.notice(f"Cog {module.name!r} is missing an EXT_METADATA variable. Assuming its a normal cog.") - # Presume Production Mode - yield module.name, True + # Presume Production Mode/Metadata defaults if metadata var does not exist. + yield module.name, (ExtMetadata.load_if_mode, ExtMetadata.no_unload) From c09247a34c6da06abaa043072f2c3cce42987b53 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 02:31:44 -0400 Subject: [PATCH 102/169] chore: fix typing --- modmail/__init__.py | 7 ------- modmail/__main__.py | 6 ++++-- modmail/bot.py | 6 ++++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index d948eaac..b9be1c46 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -1,15 +1,11 @@ import logging import logging.handlers -import typing from pathlib import Path import coloredlogs from modmail.log import ModmailLogger -if typing.TYPE_CHECKING: - from modmail.bot import ModmailBot - logging.TRACE = 5 logging.NOTICE = 25 logging.addLevelName(logging.TRACE, "TRACE") @@ -60,6 +56,3 @@ logging.getLogger("websockets").setLevel(logging.ERROR) # Set asyncio logging back to the default of INFO even if asyncio's debug mode is enabled. logging.getLogger("asyncio").setLevel(logging.INFO) - - -instance: typing.Optional["ModmailBot"] = None # Global ModmailBot instance. diff --git a/modmail/__main__.py b/modmail/__main__.py index 81af6fb4..4b4aaa84 100644 --- a/modmail/__main__.py +++ b/modmail/__main__.py @@ -1,5 +1,8 @@ import logging +from modmail.bot import ModmailBot +from modmail.log import ModmailLogger + try: # noinspection PyUnresolvedReferences from colorama import init @@ -8,9 +11,8 @@ except ImportError: pass -from modmail.bot import ModmailBot -log = logging.getLogger(__name__) +log: ModmailLogger = logging.getLogger(__name__) def main() -> None: diff --git a/modmail/bot.py b/modmail/bot.py index eb22d53f..8781125a 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -1,11 +1,13 @@ import asyncio import logging +import typing as t import arrow from aiohttp import ClientSession from discord.ext import commands from modmail.config import CONFIG, INTERNAL +from modmail.log import ModmailLogger from modmail.utils.extensions import EXTENSIONS, NO_UNLOAD, walk_extensions # noqa: F401 @@ -17,12 +19,12 @@ class ModmailBot(commands.Bot): """ main_task: asyncio.Task - logger = logging.getLogger(__name__) + logger: ModmailLogger = logging.getLogger(__name__) def __init__(self, **kwargs): self.config = CONFIG self.internal = INTERNAL - self.http_session: ClientSession = None + self.http_session: t.Optional[ClientSession] = None self.start_time = arrow.utcnow() super().__init__(command_prefix=commands.when_mentioned_or(self.config.bot.prefix), **kwargs) From f69909b1c392a0fa7e323ccc4ce2c4e89e8aa64d Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Mon, 9 Aug 2021 17:11:15 +0530 Subject: [PATCH 103/169] Enhance contributing guide Signed-off-by: onerandomusername --- CONTRIBUTING.md | 224 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 174 insertions(+), 50 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfab4ceb..fa048781 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,80 +1,204 @@ -# Contributing to Modmail +# Contributing Guidelines +Thank you so much for your interest in contributing!. All types of contributions are encouraged and valued. See below for different ways to help, and details about how this project handles them! -Welcome to Modmail, and thank you for your interest in contributing! +Please make sure to read the relevant section before making your contribution! It will make it a lot easier for us maintainers to make the most of it and smooth out the experience for all involved. πŸ’š -## Note -We recommend checking open issues for something you would like to work on. If something is interesting, leave a comment and wait for a person to assign you, otherwise someone else may already be working on the same thing. +> **NOTE**: that failing to comply with our guidelines may lead to a rejection of the contribution. -## Installation +If you are confused by any of these rules, feel free to ask us in the `#support` channel in our [Discord server.](https://discord.gg/ERteGkedDW) -To get started, you need: +## How do I... +* Ask or Say Something πŸ€”πŸ›πŸ˜± + * [Request Support](#request-support) + * [Report an Error or Bug](#report-an-error-or-bug) + * [Request a Feature](#request-a-feature) +* Make Something πŸ€“πŸ‘©πŸ½β€πŸ’»πŸ“œπŸ³ + * [Project Setup](#project-setup) + * [Contribute Code](#contribute-code) +* Style Guides βœ…πŸ™†πŸΌπŸ’ƒπŸ‘” + * [Git Commit Messages](#git-commit-messages) + * [Python Styleguide](#python-styleguide) -- git -- python 3.8 or higher -- poetry +## Request Support -These are the bare minimum requirements, as poetry is used to install everything needed by our project. You can find installation instructions on [poetry's website][poetry-install]. +* You can either ask your question as issue by opening one at https://github.com/discord-modmail/modmail/issues. -If you have write access to this repo, you may make a new branch and push changes to it here. Otherwise, make a [fork][], and clone it locally to push your changes to. Be sure to push your changes to a new branch. +* [Join the Modmail Discord Server](https://discord.gg/ERteGkedDW) + * Even though Discord is a chat service, sometimes it takes several hours for community members to respond — please be patient! + * Use the `#support` channel for questions or discussion about writing or contributing to Discord Modmail bot. + * There are many other channels available, check the channel list +## Report an Error or Bug -To install all dependencies, run the following command. -```sh -poetry install -``` -This will create a venv for our project, and install everything to the venv. +If you run into an error or bug with the project: +> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. -Next, install our pre-commit hook, which will help ensure all commits follow our coding guidelines. +* Open an Issue at https://github.com/discord-modmail/modmail/issues +* Explain the problem and include additional details to help maintainers reproduce the problem: + * **Use a clear and descriptive title** for the issue to identify the problem. + * **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. + * **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/paste-able snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). + * **Describe the behaviour you observed after following the steps** and point out what exactly is the problem with that behaviour. + * **Explain which behaviour you expected to see instead and why.** + * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) on Linux (ofcourse there are plenty more). -```sh -poetry run task precommit -``` +## Request a Feature -This installs [pre-commit][] to git's hooks, and all of the tools too. +If the project doesn't do something you need or want it to do: -## Testing +* Open an Issue at https://github.com/discord-modmail/modmail/issues +* Provide as much context as you can about what you're running into. + * **Use a clear and descriptive title** for the issue to identify the suggestion. + * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. + * **Provide specific examples to demonstrate the steps**. Include copy/paste-able snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). + * **Explain why this enhancement would be useful** to Modmail, and would benefit the community members. +* Please try and be clear about why existing features and alternatives would not work for you. -We use pytest for our testing framework, and to run our tests. +Once it's filed: -If you are adding features, we expect tests to be written, and passing. +* The Maintainers will [label the issue](#label-issues). +* The Maintainers will evaluate the feature request, possibly asking you more questions to understand its purpose and any relevant requirements. If the issue is closed, the team will convey their reasoning and suggest an alternative path forward. +* If the feature request is accepted, it will be marked for implementation with `status: approved`, which can then be done by either by a core team member or by anyone in the community who wants to contribute code. -To run the entire test suite, use -```sh -poetry run task test -``` -If you would like to run just a specific file's test, use -```sh -poetry run task test {file} +> **Note**: The team is unlikely to be able to accept every single feature request that is filed. Please understand if they need to say no. + +## Project Setup + +So you wanna contribute some code! That's great! This project uses GitHub Pull Requests to manage contributions, so [read up on how to fork a GitHub project and file a PR](https://guides.github.com/activities/forking) if you've never done it before. + + +### Test Server and Bot Account +You will need your own test server and bot account on Discord to test your changes to the bot. + +1. Create a test server. +2. Create a bot account and invite it to the server you just created. + +Note down the IDs for your server, as well as any channels and roles created. +Learn how to obtain the ID of a server, channel or role **[here](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-).** + + +### Fork the Project +You will need your own remote (online) copy of the project repository, known as a fork. +You will do all your work in the fork rather than directly in the main repository. + +And you should be ready to go! + +#### Once You Have Your Fork, You Will Need To Clone The Repository To Your Computer. +```shell +$ git clone https://github.com//modmail +... +$ cd modmail ``` -## Run the bot +**or using the github CLI** +```shell +$ gh repo clone /modmail +... +$ cd modmail +``` -To run the bot, use: -```sh -poetry run task run +### After cloning, proceed to install the project's dependencies. +Make sure you are in the project directory. +```shell +# This will install the development and project dependencies. +poetry install +# This will install the pre-commit hooks. +poetry run task precommit +# Optionally: run pre-commit hooks to initialize them. +poetry run task lint ``` -## Tasks +### After installing dependencies, you will have to setup environment variables: +1. Create a text file named .env in your project root (that's the base folder of your repository): + +> Note: The entire file name is literally .env + +2. Open the file with any text editor. +3. Each environment variable is on its own line, with the variable and the value separated by a `=` sign. + -We use [taskipy][] to run a bunch of our common commands. These are subject to change, but you can always get an up to date list by running the following: -```sh -poetry run task --list + +### The following variables are needed for running Modmail: + +|ENV VARIABLE NAME |WHAT IS IT? | +|------------------|-------------------------------------------------------------------------| +|TOKEN |Bot Token from the Discord developer portal | +|GUILD_ID |ID of the discord server | + +Rest of them can be viewed here: https://github.com/discord-modmail/modmail/blob/main/modmail/config.py + + +### Run The Project +To run the project, use the (below) in the project root. + +```shell +$ poetry run task run ``` -## A note on Poetry +## Contribute Code +We like code commits a lot! They're super handy, and they keep the project going and doing the work it needs to do to be useful to others. + +Code contributions of just about any size are acceptable! + +To contribute code: + +* [Set up the project](#project-setup). +* Make any necessary changes to the source code. +* Write clear, concise commit message(s). + * A more in-depth guide to writing great commit messages can be found in Chris Beam's [*How to Write a Git Commit Message*](https://chris.beams.io/posts/git-commit/). +* Run `flake8`, `black` and `pre-commit` against your code **before** you push. Your commit will be rejected by the build server if it fails to lint. You can run the lint by executing `poetry run task lint` in your command line. +* Go to https://github.com/discord-modmail/modmail/pulls and open a new pull request with your changes. +* If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This gives permission for maintainers to commit changes directly to your fork, speeding up the review process. +* If your PR is connected to an open issue, add a line in your PR's description that says `Closes #123`, where `#123` is the number of the issue you're fixing. + +> Pull requests (or PRs for short) are the primary mechanism we use to change Rust. GitHub itself has some [great documentation][about-pull-requests] on using the Pull Request feature. We use the "fork and pull" model [described here][development-models], where contributors push changes to their personal fork and create pull requests to bring those changes into the source repository. + +[about-pull-requests]: https://help.github.com/articles/about-pull-requests/ +[development-models]: https://help.github.com/articles/about-collaborative-development-models/ + +Once you've filed the PR: + +* Barring special circumstances, maintainers will not review PRs until lint checks pass (`poetry run task lint`). +* One or more contributors will use GitHub's review feature to review your PR. +* If the maintainer asks for any changes, edit your changes, push, and ask for another review. +* If the maintainer decides to pass on your PR, they will thank you for the contribution and explain why they won't be accepting the changes. That's ok! We still really appreciate you taking the time to do it, and we don't take that lightly. πŸ’š +* If your PR gets accepted, it will be marked as such, and merged into the `main` branch soon after. + + +## Git Commit Messages + +Commit messages must start with a capitalized and short summary (max. 50 chars) +written in the imperative, followed by an optional, more detailed explanatory +text which is separated from the summary by an empty line. + +Commit messages should follow best practices, including explaining the context +of the problem and how it was solved, including in caveats or follow up changes +required. They should tell the story of the change and provide readers +understanding of what led to it. + +If you're lost about what this even means, please see [How to Write a Git +Commit Message](http://chris.beams.io/posts/git-commit/) for a start. + +In practice, the best approach to maintaining a nice commit message is to +leverage a `git add -p` and `git commit --amend` to formulate a solid +changeset. This allows one to piece together a change, as information becomes +available. + +If you squash a series of commits, don't just submit that. Re-write the commit +message, as if the series of commits was a single stroke of brilliance. -All of the commands in this page use `poetry run` for the sake of clarity. However, it is possible to use **`poetry shell`** to enter the venv and therefore not require `poetry run` before every command. +That said, there is no requirement to have a single commit for a PR, as long as +each commit tells the story. For example, if there is a feature that requires a +package, it might make sense to have the package in a separate commit then have +a subsequent commit that uses it. -------- +Remember, you're telling part of the story with the commit message. Don't make +your chapter weird. -## Submit Changes +## Python Styleguide +WIP... -To submit your changes, go to the [pulls][] page, select your branch, and create a new pull request pointed towards our repository. -We recommend creating a pull request even while it is in progress, so we can see it as it goes on. +## Attribution -[fork]: https://github.com/discord-modmail/modmail/fork -[poetry-install]: https://python-poetry.org/docs#installation -[pre-commit]: https://pre-commit.com/ -[pulls]: https://github.com/discord-modmail/modmail/pulls -[taskipy]: https://pypi.org/project/taskipy/ +This contributing guide is inspired by the [Moby's](https://github.com/moby/moby) and [Atom Text Editor's](https://github.com/atom/atom) contributing guide. From da0e6d927e6d00f6c033417456abe8443dcf0c36 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 13:10:49 -0400 Subject: [PATCH 104/169] rename task run to task start --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5c3faee4..63c8aeec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ lint = { cmd = "pre-commit run --all-files", help = "Checks all files for CI err precommit = { cmd = "pre-commit install --install-hooks", help = "Installs the precommit hook" } pytest-docs = { cmd = "pytest --no-cov --docs tests/docs.md", help = "Create docs for tests using pytest-docs."} report = { cmd = "coverage report", help = "Show coverage report from previously run tests." } -run = { cmd = "python -m modmail", help = "Run bot" } +start = { cmd = "python -m modmail", help = "Run bot" } test = { cmd = "pytest -n auto --dist loadfile --cov-report= --cov= --docs tests/docs.md", help = "Runs tests and save results to a coverage report" } test-nocov = { cmd = "pytest --no-cov", help = "Runs tests without creating a coverage report" } From d1030b2e88ece1e0a5795e803e8d2c5b9ab3caea Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 13:12:28 -0400 Subject: [PATCH 105/169] nitpicks to CONTRIBUTING.md --- CONTRIBUTING.md | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa048781..54a78773 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ Thank you so much for your interest in contributing!. All types of contributions Please make sure to read the relevant section before making your contribution! It will make it a lot easier for us maintainers to make the most of it and smooth out the experience for all involved. πŸ’š -> **NOTE**: that failing to comply with our guidelines may lead to a rejection of the contribution. +> **NOTE**: failing to comply with our guidelines may lead to a rejection of the contribution. If you are confused by any of these rules, feel free to ask us in the `#support` channel in our [Discord server.](https://discord.gg/ERteGkedDW) @@ -82,22 +82,28 @@ Learn how to obtain the ID of a server, channel or role **[here](https://support You will need your own remote (online) copy of the project repository, known as a fork. You will do all your work in the fork rather than directly in the main repository. +You can click [here to fork][fork] + And you should be ready to go! -#### Once You Have Your Fork, You Will Need To Clone The Repository To Your Computer. +[fork]: https://github.com/discord-modmail/modmail/fork + +Once you have your fork you will need to clone the repo to your computer. ```shell $ git clone https://github.com//modmail ... $ cd modmail ``` -**or using the github CLI** +or using the [github cli](https://github.com/cli/cli): ```shell $ gh repo clone /modmail ... $ cd modmail ``` +> Tip: You can use the github cli to fork the repo as well, just use `gh repo fork discord-modmail/modmail` and it will allow you to clone it directly. + ### After cloning, proceed to install the project's dependencies. Make sure you are in the project directory. ```shell @@ -105,17 +111,16 @@ Make sure you are in the project directory. poetry install # This will install the pre-commit hooks. poetry run task precommit -# Optionally: run pre-commit hooks to initialize them. -poetry run task lint ``` ### After installing dependencies, you will have to setup environment variables: -1. Create a text file named .env in your project root (that's the base folder of your repository): +1. Create a text file named `.env` in your project root (that's the base folder of your repository): + - You can also copy the `.env.example` file to `.env` -> Note: The entire file name is literally .env +> Note: The entire file name is literally `.env` -2. Open the file with any text editor. -3. Each environment variable is on its own line, with the variable and the value separated by a `=` sign. +1. Open the file with any text editor. +2. Each environment variable is on its own line, with the variable and the value separated by a `=` sign. @@ -124,16 +129,15 @@ poetry run task lint |ENV VARIABLE NAME |WHAT IS IT? | |------------------|-------------------------------------------------------------------------| |TOKEN |Bot Token from the Discord developer portal | -|GUILD_ID |ID of the discord server | -Rest of them can be viewed here: https://github.com/discord-modmail/modmail/blob/main/modmail/config.py +The rest of them can be viewed in our example file. [.env.example](./.env.example) ### Run The Project To run the project, use the (below) in the project root. ```shell -$ poetry run task run +$ poetry run task start ``` ## Contribute Code @@ -152,7 +156,7 @@ To contribute code: * If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This gives permission for maintainers to commit changes directly to your fork, speeding up the review process. * If your PR is connected to an open issue, add a line in your PR's description that says `Closes #123`, where `#123` is the number of the issue you're fixing. -> Pull requests (or PRs for short) are the primary mechanism we use to change Rust. GitHub itself has some [great documentation][about-pull-requests] on using the Pull Request feature. We use the "fork and pull" model [described here][development-models], where contributors push changes to their personal fork and create pull requests to bring those changes into the source repository. +> Pull requests (or PRs for short) are the primary mechanism we use to change modmail. GitHub itself has some [great documentation][about-pull-requests] on using the Pull Request feature. We use the "fork and pull" model [described here][development-models], where contributors push changes to their personal fork and create pull requests to bring those changes into the source repository. [about-pull-requests]: https://help.github.com/articles/about-pull-requests/ [development-models]: https://help.github.com/articles/about-collaborative-development-models/ @@ -168,7 +172,7 @@ Once you've filed the PR: ## Git Commit Messages -Commit messages must start with a capitalized and short summary (max. 50 chars) +Commit messages must start with a short summary (max. 50 chars) written in the imperative, followed by an optional, more detailed explanatory text which is separated from the summary by an empty line. @@ -177,6 +181,8 @@ of the problem and how it was solved, including in caveats or follow up changes required. They should tell the story of the change and provide readers understanding of what led to it. +Check out [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for more information. + If you're lost about what this even means, please see [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/) for a start. From 3907cb0352ac17e5c16b363f1ab5bed92814481e Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 13:32:07 -0400 Subject: [PATCH 106/169] fix ext blacklist when unloading with * --- modmail/extensions/extension_manager.py | 8 +++++--- modmail/extensions/plugin_manager.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index b06e98b4..7d4566d3 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -85,7 +85,7 @@ def __init__(self, bot: ModmailBot): self.bot = bot self.all_extensions = EXTENSIONS - async def get_black_listed_extensions(self) -> list: + def get_black_listed_extensions(self) -> list: """Returns a list of all unload blacklisted extensions.""" return NO_UNLOAD @@ -122,7 +122,7 @@ async def unload_extensions(self, ctx: Context, *extensions: ExtensionConverter) await ctx.send_help(ctx.command) return - blacklisted = [ext for ext in await self.get_black_listed_extensions() if ext in extensions] + blacklisted = [ext for ext in self.get_black_listed_extensions() if ext in extensions] if blacklisted: bl_msg = "\n".join(blacklisted) @@ -130,7 +130,9 @@ async def unload_extensions(self, ctx: Context, *extensions: ExtensionConverter) return if "*" in extensions or "**" in extensions: - extensions = sorted(ext for ext in self.bot.extensions.keys() if ext not in blacklisted) + extensions = sorted( + ext for ext in self.bot.extensions.keys() if ext not in self.get_black_listed_extensions() + ) await ctx.send(self.batch_manage(Action.UNLOAD, *extensions)) diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 9c5cf4f1..12ed5faf 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -30,7 +30,7 @@ def __init__(self, bot: ModmailBot) -> None: super().__init__(bot) self.all_extensions = PLUGINS - async def get_black_listed_extensions(self) -> list: + def get_black_listed_extensions(self) -> list: """ Returns a list of all unload blacklisted plugins. From 444dccf34d429c3b26c26fa3969a55996a403994 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 13:42:11 -0400 Subject: [PATCH 107/169] recursively scan plugin folder for plugins --- modmail/utils/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/utils/plugins.py b/modmail/utils/plugins.py index 83af6296..990fe647 100644 --- a/modmail/utils/plugins.py +++ b/modmail/utils/plugins.py @@ -29,7 +29,7 @@ def walk_plugins() -> Iterator[str]: """Yield plugin names from the modmail.plugins subpackage.""" - for path in BASE_PATH.glob("*/*.py"): + for path in BASE_PATH.glob("**/*.py"): # calculate the module name, if it were to have a name from the path relative_path = path.relative_to(BASE_PATH) name = relative_path.__str__().rstrip(".py").replace("/", ".") From d9a7bd89fbc83c0a69c3731b9009f13af4488ecc Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 15:46:54 -0400 Subject: [PATCH 108/169] rename .env.example to .env.template --- .env.example => .env.template | 0 CONTRIBUTING.md | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename .env.example => .env.template (100%) diff --git a/.env.example b/.env.template similarity index 100% rename from .env.example rename to .env.template diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54a78773..fc61a161 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,7 +115,7 @@ poetry run task precommit ### After installing dependencies, you will have to setup environment variables: 1. Create a text file named `.env` in your project root (that's the base folder of your repository): - - You can also copy the `.env.example` file to `.env` + - You can also copy the `.env.template` file to `.env` > Note: The entire file name is literally `.env` @@ -130,7 +130,7 @@ poetry run task precommit |------------------|-------------------------------------------------------------------------| |TOKEN |Bot Token from the Discord developer portal | -The rest of them can be viewed in our example file. [.env.example](./.env.example) +The rest of them can be viewed in our example file. [.env.template](./.env.template) ### Run The Project From 8a53b7283cda384af1781ce7618c1812d2681cb3 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 16:02:04 -0400 Subject: [PATCH 109/169] allow colored logs environment override for logging colors --- modmail/__init__.py | 21 ++++++++++++++++++--- poetry.lock | 42 +++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 3 +++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index b9be1c46..df242725 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -6,6 +6,18 @@ from modmail.log import ModmailLogger +# this block allows coloredlogs coloring to be overidden by the enviroment variable. +# coloredlogs contains support for it, but strangely does not default to the enviroment overriding. +try: + # import the enviroment package + from environs import Env +except ImportError: + COLOREDLOGS_LEVEL_STYLES = None +else: + env = Env() + env.read_env("./env") + COLOREDLOGS_LEVEL_STYLES = env.str("COLOREDLOGS_LEVEL_STYLES", None) + logging.TRACE = 5 logging.NOTICE = 25 logging.addLevelName(logging.TRACE, "TRACE") @@ -40,9 +52,12 @@ file_handler.setLevel(logging.TRACE) -# configure trace color -LEVEL_STYLES = dict(coloredlogs.DEFAULT_LEVEL_STYLES) -LEVEL_STYLES["trace"] = LEVEL_STYLES["spam"] +# configure trace color if the env var is not configured +if COLOREDLOGS_LEVEL_STYLES is None: + LEVEL_STYLES = coloredlogs.DEFAULT_LEVEL_STYLES + LEVEL_STYLES["trace"] = LEVEL_STYLES["spam"] +else: + LEVEL_STYLES = None coloredlogs.install(level=logging.TRACE, fmt=FMT, datefmt=DATEFMT, level_styles=LEVEL_STYLES) diff --git a/poetry.lock b/poetry.lock index d92895c9..93c225dc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -284,6 +284,24 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "environs" +version = "9.3.3" +description = "simplified environment variable parsing" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=3.0.0" +python-dotenv = "*" + +[package.extras] +dev = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "tox"] +django = ["dj-database-url", "dj-email-url", "django-cache-url"] +lint = ["flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url"] + [[package]] name = "execnet" version = "1.9.0" @@ -503,6 +521,20 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] +[[package]] +name = "marshmallow" +version = "3.13.0" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.910)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.1.1)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.6)"] +lint = ["mypy (==0.910)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + [[package]] name = "mccabe" version = "0.6.1" @@ -1015,7 +1047,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "6735433ebc54f95f08d6b83877c0fee831ea13af835a5e4cfaf3310d41f83157" +content-hash = "1a87eb98188b244523289104cb26d3fb50e4222f0dc946d199b8d5daef959e61" [metadata.files] aiodns = [ @@ -1302,6 +1334,10 @@ distlib = [ {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, ] +environs = [ + {file = "environs-9.3.3-py2.py3-none-any.whl", hash = "sha256:ee5466156b50fe03aa9fec6e720feea577b5bf515d7f21b2c46608272557ba26"}, + {file = "environs-9.3.3.tar.gz", hash = "sha256:72b867ff7b553076cdd90f3ee01ecc1cf854987639c9c459f0ed0d3d44ae490c"}, +] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, @@ -1376,6 +1412,10 @@ isort = [ {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, ] +marshmallow = [ + {file = "marshmallow-3.13.0-py2.py3-none-any.whl", hash = "sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842"}, + {file = "marshmallow-3.13.0.tar.gz", hash = "sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, diff --git a/pyproject.toml b/pyproject.toml index 63c8aeec..718921aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,9 @@ pydantic = "^1.8.2" python-dotenv = "~=0.17.1" toml = "^0.10.2" +[tool.poetry.group.dev.dependencies] +environs = "^9.3.3" + [tool.poetry.dev-dependencies] # always needed pre-commit = "~=2.1" From a032737379d4c73cba1aee5aeaaca1ad2a9835f1 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 16:19:31 -0400 Subject: [PATCH 110/169] make pyproject.toml follow the 1.1.x poetry spec --- poetry.lock | 13 ++++++++----- pyproject.toml | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 93c225dc..eb7ddd5a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -288,8 +288,8 @@ python-versions = "*" name = "environs" version = "9.3.3" description = "simplified environment variable parsing" -category = "dev" -optional = false +category = "main" +optional = true python-versions = ">=3.6" [package.dependencies] @@ -525,8 +525,8 @@ plugins = ["setuptools"] name = "marshmallow" version = "3.13.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -category = "dev" -optional = false +category = "main" +optional = true python-versions = ">=3.5" [package.extras] @@ -1044,10 +1044,13 @@ python-versions = ">=3.6" idna = ">=2.0" multidict = ">=4.0" +[extras] +environs = ["environs"] + [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "1a87eb98188b244523289104cb26d3fb50e4222f0dc946d199b8d5daef959e61" +content-hash = "dcce698f7c219959ea613dd2d201ba902cd07ba41efe420bda0eb3140c3d91fe" [metadata.files] aiodns = [ diff --git a/pyproject.toml b/pyproject.toml index 718921aa..b86d5009 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,12 +22,13 @@ arrow = "^1.1.1" colorama = "^0.4.3" coloredlogs = "^15.0" "discord.py" = {git = "https://github.com/Rapptz/discord.py.git", rev = "master"} +environs = {version = "~=9.3.3", optional = true} pydantic = "^1.8.2" python-dotenv = "~=0.17.1" toml = "^0.10.2" -[tool.poetry.group.dev.dependencies] -environs = "^9.3.3" +[tool.poetry.extras] +environs = ["environs"] [tool.poetry.dev-dependencies] # always needed From 3f219bd9f949435eef81e1fc7ba982ef41f16b93 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 18:20:00 -0400 Subject: [PATCH 111/169] plugins: change logic to always unload plugins before extensions --- modmail/bot.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 8781125a..78642a82 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -9,6 +9,7 @@ from modmail.config import CONFIG, INTERNAL from modmail.log import ModmailLogger from modmail.utils.extensions import EXTENSIONS, NO_UNLOAD, walk_extensions # noqa: F401 +from modmail.utils.plugins import PLUGINS, walk_plugins class ModmailBot(commands.Bot): @@ -33,7 +34,14 @@ async def create_session(self) -> None: self.http_session = ClientSession() async def close(self) -> None: - """Safely close HTTP session and extensions when bot is shutting down.""" + """Safely close HTTP session and unload plugins and extensions when bot is shutting down.""" + plugins = self.extensions & PLUGINS.keys() + for plug in list(plugins): + try: + self.unload_extension(plug) + except Exception: + self.logger.error(f"Exception occured while unloading plugin {plug.name}", exc_info=1) + for ext in list(self.extensions): try: self.unload_extension(ext) @@ -67,9 +75,8 @@ def load_extensions(self) -> None: def load_plugins(self) -> None: """Load all enabled plugins.""" - from modmail.utils.plugins import PLUGINS, walk_plugins - PLUGINS.update(walk_plugins()) + for plugin, should_load in PLUGINS.items(): if should_load: self.logger.debug(f"Loading plugin {plugin}") From 7c7142e3ebd4ee983f77eb49e12280fc1af10f41 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 18:50:14 -0400 Subject: [PATCH 112/169] metadata: ensure metadata is actually acted on extensions which were set to dev mode only were loading anyways --- modmail/bot.py | 4 ++-- modmail/utils/plugins.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 78642a82..596e187d 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -68,8 +68,8 @@ def load_extensions(self) -> None: if value[1]: NO_UNLOAD.append(ext) - for extension, should_load in EXTENSIONS.items(): - if should_load: + for extension, value in EXTENSIONS.items(): + if value[0]: self.logger.debug(f"Loading extension {extension}") self.load_extension(extension) diff --git a/modmail/utils/plugins.py b/modmail/utils/plugins.py index 990fe647..2388ab5a 100644 --- a/modmail/utils/plugins.py +++ b/modmail/utils/plugins.py @@ -63,4 +63,5 @@ def walk_plugins() -> Iterator[str]: f"Plugin {imported.__name__!r} is missing a EXT_METADATA variable. Assuming its a normal plugin." ) - yield imported.__name__, True + # Presume Production Mode/Metadata defaults if metadata var does not exist. + yield imported.__name__, ExtMetadata.load_if_mode From b7e1e5179d72db64cc57d851a6c2a11a081436b6 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 23:35:40 -0400 Subject: [PATCH 113/169] chore: update typing --- modmail/bot.py | 6 +++--- modmail/utils/extensions.py | 10 +++++----- modmail/utils/plugins.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 596e187d..c63b57bf 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -40,19 +40,19 @@ async def close(self) -> None: try: self.unload_extension(plug) except Exception: - self.logger.error(f"Exception occured while unloading plugin {plug.name}", exc_info=1) + self.logger.error(f"Exception occured while unloading plugin {plug.name}", exc_info=True) for ext in list(self.extensions): try: self.unload_extension(ext) except Exception: - self.logger.error(f"Exception occured while unloading {ext.name}", exc_info=1) + self.logger.error(f"Exception occured while unloading {ext.name}", exc_info=True) for cog in list(self.cogs): try: self.remove_cog(cog) except Exception: - self.logger.error(f"Exception occured while removing cog {cog.name}", exc_info=1) + self.logger.error(f"Exception occured while removing cog {cog.name}", exc_info=True) if self.http_session: await self.http_session.close() diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 25eb0c82..6c74fc88 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -5,7 +5,7 @@ import inspect import logging import pkgutil -from typing import Iterator, NoReturn, Tuple +import typing as t from modmail import extensions from modmail.config import CONFIG @@ -17,8 +17,8 @@ EXT_METADATA = ExtMetadata -EXTENSIONS = dict() -NO_UNLOAD = list() +EXTENSIONS: t.Dict[str, t.Tuple[bool, bool]] = dict() +NO_UNLOAD: t.List[str] = list() def determine_bot_mode() -> int: @@ -47,10 +47,10 @@ def unqualify(name: str) -> str: return name.rsplit(".", maxsplit=1)[-1] -def walk_extensions() -> Iterator[Tuple]: +def walk_extensions() -> t.Iterator[t.Tuple[str, t.Tuple[bool, bool]]]: """Yield extension names from the modmail.exts subpackage.""" - def on_error(name: str) -> NoReturn: + def on_error(name: str) -> t.NoReturn: raise ImportError(name=name) # pragma: no cover for module in pkgutil.walk_packages(extensions.__path__, f"{extensions.__name__}.", onerror=on_error): diff --git a/modmail/utils/plugins.py b/modmail/utils/plugins.py index 2388ab5a..dcb19377 100644 --- a/modmail/utils/plugins.py +++ b/modmail/utils/plugins.py @@ -12,8 +12,8 @@ import importlib.util import inspect import logging +import typing as t from pathlib import Path -from typing import Iterator from modmail import plugins from modmail.log import ModmailLogger @@ -24,10 +24,10 @@ log: ModmailLogger = logging.getLogger(__name__) -PLUGINS = dict() +PLUGINS: t.Dict[str, t.Tuple[bool, bool]] = dict() -def walk_plugins() -> Iterator[str]: +def walk_plugins() -> t.Iterator[t.Tuple[str, bool]]: """Yield plugin names from the modmail.plugins subpackage.""" for path in BASE_PATH.glob("**/*.py"): # calculate the module name, if it were to have a name from the path From 6ef4cc66a6edf6a995b010657960adbbbe8acf7f Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Mon, 9 Aug 2021 23:46:00 -0400 Subject: [PATCH 114/169] fix: don't crash entire bot if a plugin fails loading --- modmail/bot.py | 7 ++++++- modmail/utils/plugins.py | 23 +++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index c63b57bf..06c0490e 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -80,7 +80,12 @@ def load_plugins(self) -> None: for plugin, should_load in PLUGINS.items(): if should_load: self.logger.debug(f"Loading plugin {plugin}") - self.load_extension(plugin) + try: + # since we're loading user generated content, + # any errors here will take down the entire bot + self.load_extension(plugin) + except Exception: + self.logger.error("Failed to load plugin {0}".format(plugin)) def add_cog(self, cog: commands.Cog, *, override: bool = False) -> None: """ diff --git a/modmail/utils/plugins.py b/modmail/utils/plugins.py index dcb19377..2dc7abc8 100644 --- a/modmail/utils/plugins.py +++ b/modmail/utils/plugins.py @@ -40,15 +40,26 @@ def walk_plugins() -> t.Iterator[t.Tuple[str, bool]]: # Ignore module/package names starting with an underscore. continue - # load the plugins using importlib - # this needs to be done like this, due to the fact that - # its possible a plugin will not have an __init__.py file - spec = importlib.util.spec_from_file_location(name, path) - imported = importlib.util.module_from_spec(spec) - spec.loader.exec_module(imported) + # due to the fact that plugins are user generated and may not have gone through + # the testing that the bot has, we want to ensure we try/except any plugins + # that fail to import. + try: + # load the plugins using importlib + # this needs to be done like this, due to the fact that + # its possible a plugin will not have an __init__.py file + spec = importlib.util.spec_from_file_location(name, path) + imported = importlib.util.module_from_spec(spec) + spec.loader.exec_module(imported) + except Exception: + log.error( + "Failed to import {0}. As a result, this plugin is not considered installed.".format(name), + exc_info=True, + ) + continue if not inspect.isfunction(getattr(imported, "setup", None)): # If it lacks a setup function, it's not a plugin. This is enforced by dpy. + log.trace("{0} does not have a setup function. Skipping.".format(name)) continue ext_metadata: ExtMetadata = getattr(imported, "EXT_METADATA", None) From 077d6a5024bbe18088835796cc6263aabb4568ba Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 02:54:01 -0400 Subject: [PATCH 115/169] gut configuration system --- modmail/bot.py | 3 +- modmail/config-default.toml | 111 -------------------- modmail/config.py | 198 ------------------------------------ 3 files changed, 1 insertion(+), 311 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index 06c0490e..eaf9e5cd 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -6,7 +6,7 @@ from aiohttp import ClientSession from discord.ext import commands -from modmail.config import CONFIG, INTERNAL +from modmail.config import CONFIG from modmail.log import ModmailLogger from modmail.utils.extensions import EXTENSIONS, NO_UNLOAD, walk_extensions # noqa: F401 from modmail.utils.plugins import PLUGINS, walk_plugins @@ -24,7 +24,6 @@ class ModmailBot(commands.Bot): def __init__(self, **kwargs): self.config = CONFIG - self.internal = INTERNAL self.http_session: t.Optional[ClientSession] = None self.start_time = arrow.utcnow() super().__init__(command_prefix=commands.when_mentioned_or(self.config.bot.prefix), **kwargs) diff --git a/modmail/config-default.toml b/modmail/config-default.toml index 65525306..db98e228 100644 --- a/modmail/config-default.toml +++ b/modmail/config-default.toml @@ -1,119 +1,8 @@ [bot] prefix = "?" -log_url_prefix = '/' -database_type = 'mongodb' -owners = '' -enable_plugins = true -enable_eval = true -data_collection = true -multi_bot = false - -[bot.activity] -twitch_url = "https://www.twitch.tv/discordmodmail/" - -[colors] -main_color = "#7289da" -error_color = "#e74c3c" -recipient_color = "#f1c40f" -mod_color = '#2ecc71' - -[channels] -main_category_id = "Modmail" -fallback_category_id = "Fallback Modmail" -log_channel_id = "bot-logs" -mention_channel_id = '' -update_channel_id = "0" [dev] log_level = 25 # "NOTICE" [dev.mode] production = true - -[emoji] -# sent_emoji = "white_heavy_check_mark" -# blocked_emoji = "\\N{NO ENTRY SIGN}" - -[internal] - -[mention] -alert_on_mention = false -silent_alert_on_mention = false -mention_channel_id = '' - -[snippets] -anonymous_snippets = false -use_regex_autotrigger = false - -[thread] -anon_reply_without_command = false -reply_without_command = false -plain_reply_without_command = false -mention = "@here" -user_typing = true -mod_typing = false -transfer_reactions = true -contact_silently = false -use_user_id_channel_name = false -account_age = -1 -guild_age = -1 -mod_tag = '' -show_timestamp = true - - -[thread.anon] -username = 'Staff Team' -tag = 'Response' - -[thread.auto_close] -time = 0 -silently = false -response = "This thread has been closed automatically due to inactivity after {timeout}." - -[thread.close] -footer = "Replying will create a new thread" -title = "Thread Closed" -response = "{closer.mention} has closed this Modmail thread." -on_leave = false -on_leave_reason = "The recipient has left the server." -self_close_response = "You have closed this Modmail thread." - -[thread.confirm_creation] -enabled = false -title = "Confirm thread creation" -response = "React to confirm thread creation which will directly contact the moderators" -accept_emoji = "\\N{WHITE HEAVY CHECK MARK}" -deny_emoji = "\\N{NO ENTRY SIGN}" - -[thread.cooldown] -time = 0 -embed_title = "Message not sent!" -cooldown_thread_response = "You must wait for {delta} before you can contact me again." - -[thread.creation] -response = "The staff team will get back to you as soon as possible." -footer = "Your message has been sent" -title = "Thread Created" - -[thread.disabled] -new_title = "Not Delivered" -new_response = "We are not accepting new threads." -new_footer = "Please try again later..." -current_title = "Not Delivered" -current_response = "We are not accepting any messages." -current_footer = "Please try again later..." - -[thread.move] -title = "Thread Moved" -notify = false -notify_mods = false -response = "This thread has been moved." - -[thread.self_closable] -creation_footer = "Click the lock to close the thread" -enabled = false -lock_emoji = "\\N{LOCK}" - -[updates] -disable_autoupdates = false -update_notifications = true diff --git a/modmail/config.py b/modmail/config.py index 5651a7eb..7345d25b 100644 --- a/modmail/config.py +++ b/modmail/config.py @@ -92,59 +92,15 @@ def customise_sources( ) -class ThreadBaseSettings(BaseSettings): - class Config: - env_prefix = "thread." - - # @classmethod - # def alias_generator(cls, string: str) -> str: - # return f"thread.{super.__name__}.{string}" - - -class BotActivityConfig(BaseSettings): - twitch_url: str = "https://www.twitch.tv/discordmodmail/" - - class BotConfig(BaseSettings): prefix: str = "?" - activity: BotActivityConfig token: str = None - modmail_guild_id: str = None - guild_id: str = None - multi_bot: bool = False - log_url: str = None - log_url_prefix = "/" - github_token: SecretStr = None - database_type: str = "mongodb" # TODO limit to specific strings - enable_plugins: bool = True - enable_eval: bool = True - data_collection = True - owners: str = 1 - connection_uri: str = None - level_permissions: dict = None class Config: # env_prefix = "bot." allow_mutation = False -class ColorsConfig(BaseSettings): - main_color: str = str(discord.Colour.blurple()) - error_color: str = str(discord.Colour.red()) - recipient_color: str = str(discord.Colour.green()) - mod_color: str = str(discord.Colour.blue()) - - -class ChannelConfig(BaseSettings): - # all of the below should be validated to channels - # either by name or by int - main_category: str = None - fallback_category: str = None - log_channel: str = None - mention_channel: str = None - update_channel: str = None - - class BotMode(BaseSettings): """ Bot mode. @@ -167,163 +123,9 @@ class DevConfig(BaseSettings): mode: BotMode -class EmojiConfig(BaseSettings): - """ - Standard emojis that the bot uses when a specific emoji is not defined for a specific use. - """ - - sent_emoji: str = "\\N{WHITE HEAVY CHECK MARK}" # TODO type as a discord emoji - blocked_emoji: str = "\\N{NO ENTRY SIGN}" # TODO type as a discord emoji - - -class InternalConfig(BaseModel): - # do NOT set these yourself. The bot will handle these - activity_message: str = None - activity_type: None = None - status: None = None - dm_disabled: int = 0 - # moderation - blocked: dict = dict() - blocked_roles: dict = dict() - blocked_whitelist: list = dict() - command_permissions: dict = dict() - level_permissions: dict = dict() - override_command_level: dict = dict() - # threads - snippets: dict = dict() - notifications: dict = dict() - subscriptions: dict = dict() - closures: dict = dict() - # misc - plugins: list = list() - aliases: dict = dict() - auto_triggers: dict = dict() - command_permissions: dict = dict() - level_permissions: dict = dict() - - class Config: - arbitrary_types_allowed = True - - -class MentionConfig(BaseSettings): - alert_on_mention: bool = False - silent_alert_on_mention: bool = False - mention_channel: int = None - - -class SnippetConfig(BaseSettings): - anonmous_snippets: bool = False - use_regex_autotrigger: bool = False - - -class ThreadAnonConfig(ThreadBaseSettings): - username: str = "Response" - footer: str = "Staff Team" - - -class ThreadAutoCloseConfig(ThreadBaseSettings): - time: datetime.timedelta = 0 - silently: bool = False - response: str = "This thread has been closed automatically due to inactivity after {timeout}." - - -class ThreadCloseConfig(ThreadBaseSettings): - footer: str = "Replying will create a new thread" - title: str = "Thread Closed" - response: str = "{closer.mention} has closed this Modmail thread." - on_leave: bool = False - on_leave_reason: str = "The recipient has left the server." - self_close_response: str = "You have closed this Modmail thread." - - -class ThreadConfirmCreationConfig(ThreadBaseSettings): - enabled: bool = False - title: str = "Confirm thread creation" - response: str = "React to confirm thread creation which will directly contact the moderators" - accept_emoji: str = "\N{WHITE HEAVY CHECK MARK}" # TODO type as a discord emoji - deny_emoji: str = "\N{NO ENTRY SIGN}" # TODO type as a discord emoji - - -class ThreadCooldownConfig(ThreadBaseSettings): - time: datetime.timedelta = 0 - embed_title: str = "Message not sent!" - response: str = "You must wait for {delta} before you can contact me again." - - -class ThreadCreationConfig(ThreadBaseSettings): - response: str = "The staff team will get back to you as soon as possible." - footer: str = "Your message has been sent" - title: str = "Thread Created" - - -class ThreadDisabledConfig(ThreadBaseSettings): - new_title: str = "Not Delivered" - new_response: str = "We are not accepting new threads." - new_footer: str = "Please try again later..." - current_title: str = "Not Delivered" - current_response: str = "We are not accepting any messages." - current_footer: str = "Please try again later..." - - -class ThreadMoveConfig(ThreadBaseSettings): - title: str = "Thread Moved" - notify: bool = False - notify_mods: bool = False - response: str = "This thread has been moved." - - -class ThreadSelfClosableConfig(ThreadBaseSettings): - enabled: bool = False - lock_emoji: str = "\N{LOCK}" - creation_footer: str = "Click the lock to close the thread" - - -class ThreadConfig(BaseSettings): - anon_reply_without_command: bool = False - reply_without_command: bool = False - plain_reply_without_command: bool = False - mention: str = "@here" - user_typing: bool = False - mod_typing: bool = False - transfer_reactions: bool = True - contact_silently: bool = False - account_age: datetime.timedelta = 0 - guild_age: datetime.timedelta = 0 - mod_tag: str = "" - show_timestamp: bool = True - - anon: ThreadAnonConfig - auto_close: ThreadAutoCloseConfig - close: ThreadCloseConfig - confirm_creation: ThreadConfirmCreationConfig - cooldown: ThreadCooldownConfig - creation: ThreadCreationConfig - disabled: ThreadDisabledConfig - move: ThreadMoveConfig - self_closable: ThreadSelfClosableConfig - - -class UpdateConfig(BaseSettings): - disable_autoupdates: bool = False - update_notifications: bool = True - - class Config: - allow_mutation = False - env_prefix = "updates." - - class ModmailConfig(BaseSettings): bot: BotConfig - colors: ColorsConfig - channels: ChannelConfig dev: DevConfig - emoji: EmojiConfig - mention: MentionConfig - snippets: SnippetConfig - thread: ThreadConfig - updates: UpdateConfig - shell: str = None CONFIG = ModmailConfig() -INTERNAL = InternalConfig() From cfd7332f1c3cb5f4be39b7f3c9bbd5f4471f815a Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 02:55:32 -0400 Subject: [PATCH 116/169] remove python-dotenv --- poetry.lock | 42 +++++++++++++++++++++--------------------- pyproject.toml | 1 - requirements.txt | 1 - 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/poetry.lock b/poetry.lock index eb7ddd5a..b4a03026 100644 --- a/poetry.lock +++ b/poetry.lock @@ -482,7 +482,7 @@ pyreadline = {version = "*", markers = "sys_platform == \"win32\""} [[package]] name = "identify" -version = "2.2.12" +version = "2.2.13" description = "File identification library for Python" category = "dev" optional = false @@ -604,7 +604,7 @@ python-versions = ">=2.6" [[package]] name = "pep8-naming" -version = "0.12.0" +version = "0.12.1" description = "Check PEP-8 naming conventions, plugin for flake8" category = "dev" optional = false @@ -639,7 +639,7 @@ dev = ["pre-commit", "tox"] [[package]] name = "pre-commit" -version = "2.13.0" +version = "2.14.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -871,11 +871,11 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "0.17.1" +version = "0.19.0" description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" -optional = false -python-versions = "*" +optional = true +python-versions = ">=3.5" [package.extras] cli = ["click (>=5.0)"] @@ -986,7 +986,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.0" +version = "1.2.1" description = "A lil' TOML parser" category = "dev" optional = false @@ -1015,7 +1015,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.7.0" +version = "20.7.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1050,7 +1050,7 @@ environs = ["environs"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "dcce698f7c219959ea613dd2d201ba902cd07ba41efe420bda0eb3140c3d91fe" +content-hash = "4390ff17f47b1772048e805984a4466b3420c4dbaa37b454a94a2725ff595327" [metadata.files] aiodns = [ @@ -1400,8 +1400,8 @@ humanfriendly = [ {file = "humanfriendly-9.2.tar.gz", hash = "sha256:f7dba53ac7935fd0b4a2fc9a29e316ddd9ea135fb3052d3d0279d10c18ff9c48"}, ] identify = [ - {file = "identify-2.2.12-py2.py3-none-any.whl", hash = "sha256:a510cbe155f39665625c8a4c4b4f9360cbce539f51f23f47836ab7dd852db541"}, - {file = "identify-2.2.12.tar.gz", hash = "sha256:242332b3bdd45a8af1752d5d5a3afb12bee26f8e67c4be06e394f82d05ef1a4d"}, + {file = "identify-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"}, + {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, @@ -1487,8 +1487,8 @@ pbr = [ {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, ] pep8-naming = [ - {file = "pep8-naming-0.12.0.tar.gz", hash = "sha256:1f9a3ecb2f3fd83240fd40afdd70acc89695c49c333413e49788f93b61827e12"}, - {file = "pep8_naming-0.12.0-py2.py3-none-any.whl", hash = "sha256:2321ac2b7bf55383dd19a6a9c8ae2ebf05679699927a3af33e60dd7d337099d3"}, + {file = "pep8-naming-0.12.1.tar.gz", hash = "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841"}, + {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"}, ] platformdirs = [ {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"}, @@ -1499,8 +1499,8 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] pre-commit = [ - {file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"}, - {file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"}, + {file = "pre_commit-2.14.0-py2.py3-none-any.whl", hash = "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4"}, + {file = "pre_commit-2.14.0.tar.gz", hash = "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c"}, ] psutil = [ {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, @@ -1652,8 +1652,8 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-dotenv = [ - {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, - {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, + {file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"}, + {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, @@ -1735,8 +1735,8 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.0-py3-none-any.whl", hash = "sha256:056f0376bf5a6b182c513f9582c1e5b0487265eb6c48842b69aa9ca1cd5f640a"}, - {file = "tomli-1.2.0.tar.gz", hash = "sha256:d60e681734099207a6add7a10326bc2ddd1fdc36c1b0f547d00ef73ac63739c2"}, + {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, + {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, ] typing-extensions = [ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, @@ -1748,8 +1748,8 @@ urllib3 = [ {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] virtualenv = [ - {file = "virtualenv-20.7.0-py2.py3-none-any.whl", hash = "sha256:fdfdaaf0979ac03ae7f76d5224a05b58165f3c804f8aa633f3dd6f22fbd435d5"}, - {file = "virtualenv-20.7.0.tar.gz", hash = "sha256:97066a978431ec096d163e72771df5357c5c898ffdd587048f45e0aecc228094"}, + {file = "virtualenv-20.7.1-py2.py3-none-any.whl", hash = "sha256:73863dc3be1efe6ee638e77495c0c195a6384ae7b15c561f3ceb2698ae7267c1"}, + {file = "virtualenv-20.7.1.tar.gz", hash = "sha256:57bcb59c5898818bd555b1e0cfcf668bd6204bc2b53ad0e70a52413bd790f9e4"}, ] yarl = [ {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, diff --git a/pyproject.toml b/pyproject.toml index b86d5009..1f80faf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ coloredlogs = "^15.0" "discord.py" = {git = "https://github.com/Rapptz/discord.py.git", rev = "master"} environs = {version = "~=9.3.3", optional = true} pydantic = "^1.8.2" -python-dotenv = "~=0.17.1" toml = "^0.10.2" [tool.poetry.extras] diff --git a/requirements.txt b/requirements.txt index 42629b9a..f74b0a83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,6 @@ pycparser==2.20; python_version >= "3.6" and python_full_version < "3.0.0" or py pydantic==1.8.2; python_full_version >= "3.6.1" pyreadline==2.1; python_version >= "2.7" and python_full_version < "3.0.0" and sys_platform == "win32" or python_full_version >= "3.5.0" and sys_platform == "win32" python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" -python-dotenv==0.17.1 six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") typing-extensions==3.10.0.0; python_version >= "3.6" or python_full_version >= "3.6.1" or python_version >= "3.6" and python_full_version >= "3.8.0" From a9e31a7fe9e62c750be070d241306f292f3cf537 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 02:58:27 -0400 Subject: [PATCH 117/169] add todo comment --- modmail/extensions/extension_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 7d4566d3..5d0bf040 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -182,8 +182,8 @@ async def list_extensions(self, ctx: Context) -> None: log.debug(f"{ctx.author} requested a list of all {self.type}s. " "Returning a paginated list.") - # since we currently don't have a paginator. - await ctx.send("".join(lines) or f"( There are no {self.type}s installed. )") + # TODO: since we currently don't have a paginator. + await ctx.send("".join(lines) or f"There are no {self.type}s installed.") def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" From a7d2c5d9d5c94cfc4d06d1344a967de93f36d45c Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 02:59:52 -0400 Subject: [PATCH 118/169] lint: remove now unnecessary noqa comments --- modmail/bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/bot.py b/modmail/bot.py index eaf9e5cd..6ad948cb 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -8,7 +8,7 @@ from modmail.config import CONFIG from modmail.log import ModmailLogger -from modmail.utils.extensions import EXTENSIONS, NO_UNLOAD, walk_extensions # noqa: F401 +from modmail.utils.extensions import EXTENSIONS, NO_UNLOAD, walk_extensions from modmail.utils.plugins import PLUGINS, walk_plugins @@ -59,7 +59,7 @@ async def close(self) -> None: await super().close() def load_extensions(self) -> None: - """Load all enabled extensions.""" # noqa: F811 + """Load all enabled extensions.""" EXTENSIONS.update(walk_extensions()) # set up no_unload global too From abb93115f5a4731f3a851b7076fe35d9a799b52e Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 03:00:29 -0400 Subject: [PATCH 119/169] chore: fix docstring grammar --- modmail/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/bot.py b/modmail/bot.py index 6ad948cb..21a4c1d3 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -33,7 +33,7 @@ async def create_session(self) -> None: self.http_session = ClientSession() async def close(self) -> None: - """Safely close HTTP session and unload plugins and extensions when bot is shutting down.""" + """Safely close HTTP session, unload plugins and extensions when the bot is shutting down.""" plugins = self.extensions & PLUGINS.keys() for plug in list(plugins): try: From 3a7fe1b461e728f4bf37d932c1007d937198d544 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 03:12:35 -0400 Subject: [PATCH 120/169] hack: mark removing extensionmanage cmds as hack --- modmail/extensions/plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 12ed5faf..4e6702f5 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -92,7 +92,7 @@ async def cog_check(self, ctx: Context) -> bool: return ctx.author.guild_permissions.administrator or await self.bot.is_owner(ctx.author) -# Delete the commands from ExtensionManager that PluginManager has inherited +# HACK: Delete the commands from ExtensionManager that PluginManager has inherited # before discord.py tries to re-register them for command in ExtensionManager.__cog_commands__: PluginManager.__cog_commands__.remove(command) From baf05d7284812187499cd834fa2098ab4992ef62 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 03:15:01 -0400 Subject: [PATCH 121/169] chore: update cog unload docstring to reflect logger --- modmail/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/bot.py b/modmail/bot.py index 21a4c1d3..db0758db 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -107,7 +107,7 @@ def remove_cog(self, cog: str) -> None: """ Delegate to super to unregister `cog`. - This only serves to make the debug log, so that extensions don't have to. + This only serves to make the info log, so that extensions don't have to. """ super().remove_cog(cog) self.logger.info(f"Cog unloaded: {cog}") From 7aa151fba599f1a0d062dd3f85fd7f4f84638b32 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 03:32:11 -0400 Subject: [PATCH 122/169] chore: refactor environment log coloring --- modmail/__init__.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index df242725..680be253 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -6,18 +6,6 @@ from modmail.log import ModmailLogger -# this block allows coloredlogs coloring to be overidden by the enviroment variable. -# coloredlogs contains support for it, but strangely does not default to the enviroment overriding. -try: - # import the enviroment package - from environs import Env -except ImportError: - COLOREDLOGS_LEVEL_STYLES = None -else: - env = Env() - env.read_env("./env") - COLOREDLOGS_LEVEL_STYLES = env.str("COLOREDLOGS_LEVEL_STYLES", None) - logging.TRACE = 5 logging.NOTICE = 25 logging.addLevelName(logging.TRACE, "TRACE") @@ -52,14 +40,9 @@ file_handler.setLevel(logging.TRACE) -# configure trace color if the env var is not configured -if COLOREDLOGS_LEVEL_STYLES is None: - LEVEL_STYLES = coloredlogs.DEFAULT_LEVEL_STYLES - LEVEL_STYLES["trace"] = LEVEL_STYLES["spam"] -else: - LEVEL_STYLES = None +coloredlogs.DEFAULT_LEVEL_STYLES["trace"] = coloredlogs.DEFAULT_LEVEL_STYLES["spam"] -coloredlogs.install(level=logging.TRACE, fmt=FMT, datefmt=DATEFMT, level_styles=LEVEL_STYLES) +coloredlogs.install(level=logging.TRACE, fmt=FMT, datefmt=DATEFMT) # Create root logger root: ModmailLogger = logging.getLogger() From 1ce45bd671b16773d8b0c46b9284d98077203571 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 03:32:56 -0400 Subject: [PATCH 123/169] lint: remove extra lines --- modmail/extensions/extension_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 5d0bf040..e7ded405 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -196,9 +196,7 @@ def group_extension_statuses(self) -> t.Mapping[str, str]: status = ":red_circle:" root, name = ext.rsplit(".", 1) - category = " - ".join(root.split(".")) - categories[category].append(f"{status} {name}") return dict(categories) From 2ab4ba3cfbefcfd3e8ca5ffe175da23892e88187 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 03:53:16 -0400 Subject: [PATCH 124/169] fix: make the extension ul cmd not ul plugins --- modmail/extensions/extension_manager.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index e7ded405..b734cb5d 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -43,7 +43,7 @@ class ExtensionConverter(commands.Converter): source_list = EXTENSIONS type = "extension" - async def convert(self, ctx: Context, argument: str) -> str: + async def convert(self, _: Context, argument: str) -> str: """Fully qualify the name of an extension and ensure it exists.""" # Special values to reload all extensions if argument == "*" or argument == "**": @@ -77,7 +77,9 @@ async def convert(self, ctx: Context, argument: str) -> str: class ExtensionManager(ModmailCog, name="Extension Manager"): - """Extension management base class.""" + """Extension management. + + Commands to load, reload, unload, and list extensions.""" type = "extension" @@ -131,12 +133,14 @@ async def unload_extensions(self, ctx: Context, *extensions: ExtensionConverter) if "*" in extensions or "**" in extensions: extensions = sorted( - ext for ext in self.bot.extensions.keys() if ext not in self.get_black_listed_extensions() + ext + for ext in self.bot.extensions.keys() & self.all_extensions + if ext not in (self.get_black_listed_extensions()) ) await ctx.send(self.batch_manage(Action.UNLOAD, *extensions)) - @extensions_group.command(name="reload", aliases=("r",)) + @extensions_group.command(name="reload", aliases=("r", "rl")) async def reload_extensions(self, ctx: Context, *extensions: ExtensionConverter) -> None: """ Reload extensions given their fully qualified or unqualified names. From ddb46d33ff3441cdc6e421e5788dcd79fd3411c9 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 03:56:58 -0400 Subject: [PATCH 125/169] chore: fix lint --- modmail/extensions/extension_manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index b734cb5d..e7742319 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -77,9 +77,11 @@ async def convert(self, _: Context, argument: str) -> str: class ExtensionManager(ModmailCog, name="Extension Manager"): - """Extension management. + """ + Extension management. - Commands to load, reload, unload, and list extensions.""" + Commands to load, reload, unload, and list extensions. + """ type = "extension" From a5a1b21547d18a6df52420f3e08aea3b52ed78e2 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 04:11:46 -0400 Subject: [PATCH 126/169] chore: add rl as a reload alias to plugins --- modmail/extensions/plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 4e6702f5..22c58721 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -62,7 +62,7 @@ async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: """ # noqa: W605 await self.unload_extensions.callback(self, ctx, *plugins) - @plugins_group.command(name="reload", aliases=("r",)) + @plugins_group.command(name="reload", aliases=("r", "rl")) async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: """ Reload extensions given their fully qualified or unqualified names. From fa3d4f01d2d7330b6428fd977e728d4e85118381 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 04:43:14 -0400 Subject: [PATCH 127/169] ensure state of extensions is kept don't load unloaded extensions when running a reload ext command --- modmail/extensions/extension_manager.py | 18 +++++------------- modmail/extensions/plugin_manager.py | 3 +-- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index e7742319..4d26967a 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -149,18 +149,14 @@ async def reload_extensions(self, ctx: Context, *extensions: ExtensionConverter) If an extension fails to be reloaded, it will be rolled-back to the prior working state. - If '\*' is given as the name, all currently loaded extensions will be reloaded. - If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. + If '\*' or '\*\*' is given as the name, all currently loaded extensions will be reloaded. """ # noqa: W605 if not extensions: await ctx.send_help(ctx.command) return - if "**" in extensions: - extensions = self.all_extensions.keys() - elif "*" in extensions: - extensions = [*extensions, *sorted(self.bot.extensions.keys())] - extensions.remove("*") + if "*" in extensions or "**" in extensions: + extensions = self.bot.extensions.keys() & self.all_extensions.keys() await ctx.send(self.batch_manage(Action.RELOAD, *extensions)) @@ -169,7 +165,7 @@ async def list_extensions(self, ctx: Context) -> None: """ Get a list of all extensions, including their loaded status. - Grey indicates that the extension is unloaded. + Red indicates that the extension is unloaded. Green indicates that the extension is currently loaded. """ embed = Embed(colour=Colour.blurple()) @@ -244,11 +240,6 @@ def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: try: action.value(self.bot, ext) except (commands.ExtensionAlreadyLoaded, commands.ExtensionNotLoaded): - if action is Action.RELOAD: - # When reloading, just load the extension if it was not loaded. - log.debug("Treating {ext!r} as if it was not loaded.") - return self.manage(Action.LOAD, ext) - msg = f":x: {self.type.capitalize()} `{ext}` is already {verb}ed." except Exception as e: if hasattr(e, "original"): @@ -267,6 +258,7 @@ def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: # This cannot be static (must have a __func__ attribute). async def cog_check(self, ctx: Context) -> bool: """Only allow bot owners to invoke the commands in this cog.""" + # TODO: Change to allow other users to invoke this too. return await self.bot.is_owner(ctx.author) # This cannot be static (must have a __func__ attribute). diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 22c58721..341e5516 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -69,8 +69,7 @@ async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: If an extension fails to be reloaded, it will be rolled-back to the prior working state. - If '\*' is given as the name, all currently loaded extensions will be reloaded. - If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. + If '\*' or '\*\*' is given as the name, all currently loaded extensions will be reloaded. """ # noqa: W605 await self.reload_extensions.callback(self, ctx, *plugins) From 7adc7db8862d1f9e12660fa1857b55a024f8603b Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 14:24:12 -0400 Subject: [PATCH 128/169] make it easier to import Bot and Logger as a plugin --- modmail/plugin_helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modmail/plugin_helpers.py b/modmail/plugin_helpers.py index 2997b280..aa9a9be6 100644 --- a/modmail/plugin_helpers.py +++ b/modmail/plugin_helpers.py @@ -1,6 +1,8 @@ +from modmail.bot import ModmailBot +from modmail.log import ModmailLogger from modmail.utils.cogs import ModmailCog -__all__ = ["PluginCog"] +__all__ = ["PluginCog", ModmailBot, ModmailLogger] class PluginCog(ModmailCog): From 87dbc3f1a2dfecbaa9160bbdb006b8ca3ead6d8e Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 16:50:36 -0400 Subject: [PATCH 129/169] tools: exempt modmail.plugins from coverage --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1f80faf3..4d42ecab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,7 @@ test-nocov = { cmd = "pytest --no-cov", help = "Runs tests without creating a co [tool.coverage.run] branch = true source_pkgs = ["modmail"] +omit = ["modmail/plugins/**.*"] [tool.pytest.ini_options] addopts = "--cov -ra" From ecfedb3c6289acc2726f11646a921c61d5bb689f Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 16:52:29 -0400 Subject: [PATCH 130/169] tools: add markdown linter --- .pre-commit-config.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb5a22dc..6ddfe248 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,6 +15,15 @@ repos: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.8 + hooks: + - id: mdformat + exclude: "tests/docs.md" + additional_dependencies: + - mdformat-gfm + - mdformat-black + - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.9.0 hooks: From 17c23ae3a51d99226e870e2a7b9cd07a5c5a91aa Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 16:53:39 -0400 Subject: [PATCH 131/169] tools: lint markdown files --- .github/PULL_REQUEST_TEMPLATE.md | 1 + CONTRIBUTING.md | 142 ++++++++++++++++--------------- README.md | 3 +- SECURITY.md | 2 +- 4 files changed, 79 insertions(+), 69 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c8824a37..7d1d6563 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,4 +7,5 @@ We highly recommend linking to an issue that has been approved by a maintainer, ## Description + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc61a161..ea1a7ccb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,5 @@ # Contributing Guidelines + Thank you so much for your interest in contributing!. All types of contributions are encouraged and valued. See below for different ways to help, and details about how this project handles them! Please make sure to read the relevant section before making your contribution! It will make it a lot easier for us maintainers to make the most of it and smooth out the experience for all involved. πŸ’š @@ -7,26 +8,28 @@ Please make sure to read the relevant section before making your contribution! I If you are confused by any of these rules, feel free to ask us in the `#support` channel in our [Discord server.](https://discord.gg/ERteGkedDW) -## How do I... -* Ask or Say Something πŸ€”πŸ›πŸ˜± - * [Request Support](#request-support) - * [Report an Error or Bug](#report-an-error-or-bug) - * [Request a Feature](#request-a-feature) -* Make Something πŸ€“πŸ‘©πŸ½β€πŸ’»πŸ“œπŸ³ - * [Project Setup](#project-setup) - * [Contribute Code](#contribute-code) -* Style Guides βœ…πŸ™†πŸΌπŸ’ƒπŸ‘” - * [Git Commit Messages](#git-commit-messages) - * [Python Styleguide](#python-styleguide) +## How do I... + +- Ask or Say Something πŸ€”πŸ›πŸ˜± + - [Request Support](#request-support) + - [Report an Error or Bug](#report-an-error-or-bug) + - [Request a Feature](#request-a-feature) +- Make Something πŸ€“πŸ‘©πŸ½β€πŸ’»πŸ“œπŸ³ + - [Project Setup](#project-setup) + - [Contribute Code](#contribute-code) +- Style Guides βœ…πŸ™†πŸΌπŸ’ƒπŸ‘” + - [Git Commit Messages](#git-commit-messages) + - [Python Styleguide](#python-styleguide) ## Request Support -* You can either ask your question as issue by opening one at https://github.com/discord-modmail/modmail/issues. +- You can either ask your question as issue by opening one at https://github.com/discord-modmail/modmail/issues. + +- [Join the Modmail Discord Server](https://discord.gg/ERteGkedDW) -* [Join the Modmail Discord Server](https://discord.gg/ERteGkedDW) - * Even though Discord is a chat service, sometimes it takes several hours for community members to respond — please be patient! - * Use the `#support` channel for questions or discussion about writing or contributing to Discord Modmail bot. - * There are many other channels available, check the channel list + - Even though Discord is a chat service, sometimes it takes several hours for community members to respond β€” please be patient! + - Use the `#support` channel for questions or discussion about writing or contributing to Discord Modmail bot. + - There are many other channels available, check the channel list ## Report an Error or Bug @@ -34,32 +37,32 @@ If you run into an error or bug with the project: > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. -* Open an Issue at https://github.com/discord-modmail/modmail/issues -* Explain the problem and include additional details to help maintainers reproduce the problem: - * **Use a clear and descriptive title** for the issue to identify the problem. - * **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. - * **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/paste-able snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). - * **Describe the behaviour you observed after following the steps** and point out what exactly is the problem with that behaviour. - * **Explain which behaviour you expected to see instead and why.** - * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) on Linux (ofcourse there are plenty more). +- Open an Issue at [discord-modmail/modmail/issues](https://github.com/discord-modmail/modmail/issues) +- Explain the problem and include additional details to help maintainers reproduce the problem: + - **Use a clear and descriptive title** for the issue to identify the problem. + - **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. + - **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/paste-able snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). + - **Describe the behaviour you observed after following the steps** and point out what exactly is the problem with that behaviour. + - **Explain which behaviour you expected to see instead and why.** + - **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) on Linux (ofcourse there are plenty more). ## Request a Feature If the project doesn't do something you need or want it to do: -* Open an Issue at https://github.com/discord-modmail/modmail/issues -* Provide as much context as you can about what you're running into. - * **Use a clear and descriptive title** for the issue to identify the suggestion. - * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. - * **Provide specific examples to demonstrate the steps**. Include copy/paste-able snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). - * **Explain why this enhancement would be useful** to Modmail, and would benefit the community members. -* Please try and be clear about why existing features and alternatives would not work for you. +- Open an Issue at https://github.com/discord-modmail/modmail/issues +- Provide as much context as you can about what you're running into. + - **Use a clear and descriptive title** for the issue to identify the suggestion. + - **Provide a step-by-step description of the suggested enhancement** in as many details as possible. + - **Provide specific examples to demonstrate the steps**. Include copy/paste-able snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). + - **Explain why this enhancement would be useful** to Modmail, and would benefit the community members. +- Please try and be clear about why existing features and alternatives would not work for you. Once it's filed: -* The Maintainers will [label the issue](#label-issues). -* The Maintainers will evaluate the feature request, possibly asking you more questions to understand its purpose and any relevant requirements. If the issue is closed, the team will convey their reasoning and suggest an alternative path forward. -* If the feature request is accepted, it will be marked for implementation with `status: approved`, which can then be done by either by a core team member or by anyone in the community who wants to contribute code. +- The Maintainers will [label the issue](#label-issues). +- The Maintainers will evaluate the feature request, possibly asking you more questions to understand its purpose and any relevant requirements. If the issue is closed, the team will convey their reasoning and suggest an alternative path forward. +- If the feature request is accepted, it will be marked for implementation with `status: approved`, which can then be done by either by a core team member or by anyone in the community who wants to contribute code. > **Note**: The team is unlikely to be able to accept every single feature request that is filed. Please understand if they need to say no. @@ -67,18 +70,18 @@ Once it's filed: So you wanna contribute some code! That's great! This project uses GitHub Pull Requests to manage contributions, so [read up on how to fork a GitHub project and file a PR](https://guides.github.com/activities/forking) if you've never done it before. +### Test Server and Bot Account -### Test Server and Bot Account You will need your own test server and bot account on Discord to test your changes to the bot. 1. Create a test server. -2. Create a bot account and invite it to the server you just created. +1. Create a bot account and invite it to the server you just created. Note down the IDs for your server, as well as any channels and roles created. Learn how to obtain the ID of a server, channel or role **[here](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-).** +### Fork the Project -### Fork the Project You will need your own remote (online) copy of the project repository, known as a fork. You will do all your work in the fork rather than directly in the main repository. @@ -86,9 +89,8 @@ You can click [here to fork][fork] And you should be ready to go! -[fork]: https://github.com/discord-modmail/modmail/fork - Once you have your fork you will need to clone the repo to your computer. + ```shell $ git clone https://github.com//modmail ... @@ -96,6 +98,7 @@ $ cd modmail ``` or using the [github cli](https://github.com/cli/cli): + ```shell $ gh repo clone /modmail ... @@ -104,8 +107,10 @@ $ cd modmail > Tip: You can use the github cli to fork the repo as well, just use `gh repo fork discord-modmail/modmail` and it will allow you to clone it directly. -### After cloning, proceed to install the project's dependencies. +### Install development dependencies + Make sure you are in the project directory. + ```shell # This will install the development and project dependencies. poetry install @@ -113,27 +118,28 @@ poetry install poetry run task precommit ``` -### After installing dependencies, you will have to setup environment variables: +### Set up environment variables + 1. Create a text file named `.env` in your project root (that's the base folder of your repository): - You can also copy the `.env.template` file to `.env` > Note: The entire file name is literally `.env` 1. Open the file with any text editor. -2. Each environment variable is on its own line, with the variable and the value separated by a `=` sign. - +1. Each environment variable is on its own line, with the variable and the value separated by a `=` sign. +#### The following variables are needed for running Modmail: -### The following variables are needed for running Modmail: - -|ENV VARIABLE NAME |WHAT IS IT? | -|------------------|-------------------------------------------------------------------------| -|TOKEN |Bot Token from the Discord developer portal | +| Required | ENV VARIABLE NAME | TYPE | WHAT IS IT? | +| -------- | ----------------- | ------- | ------------------------------------------------ | +| True | `TOKEN` | String | Bot Token from the Discord developer portal | +| False | `DEVELOP` | Boolean | Enables the developer bot extensions | +| False | `PLUGIN_DEV` | Boolean | Enables plugin-developer friendly bot extensions | The rest of them can be viewed in our example file. [.env.template](./.env.template) +### Run The Project -### Run The Project To run the project, use the (below) in the project root. ```shell @@ -141,34 +147,31 @@ $ poetry run task start ``` ## Contribute Code + We like code commits a lot! They're super handy, and they keep the project going and doing the work it needs to do to be useful to others. Code contributions of just about any size are acceptable! To contribute code: -* [Set up the project](#project-setup). -* Make any necessary changes to the source code. -* Write clear, concise commit message(s). - * A more in-depth guide to writing great commit messages can be found in Chris Beam's [*How to Write a Git Commit Message*](https://chris.beams.io/posts/git-commit/). -* Run `flake8`, `black` and `pre-commit` against your code **before** you push. Your commit will be rejected by the build server if it fails to lint. You can run the lint by executing `poetry run task lint` in your command line. -* Go to https://github.com/discord-modmail/modmail/pulls and open a new pull request with your changes. -* If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This gives permission for maintainers to commit changes directly to your fork, speeding up the review process. -* If your PR is connected to an open issue, add a line in your PR's description that says `Closes #123`, where `#123` is the number of the issue you're fixing. +- [Set up the project](#project-setup). +- Make any necessary changes to the source code. +- Write clear, concise commit message(s). + - A more in-depth guide to writing great commit messages can be found in Chris Beam's [*How to Write a Git Commit Message*](https://chris.beams.io/posts/git-commit/). +- Run `flake8`, `black` and `pre-commit` against your code **before** you push. Your commit will be rejected by the build server if it fails to lint. You can run the lint by executing `poetry run task lint` in your command line. +- Go to [discord-modmail/modmail/pulls](https://github.com/discord-modmail/modmail/pulls) and open a new pull request with your changes. +- If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This permits maintainers to commit changes directly to your fork, speeding up the review process. +- If your PR is connected to an open issue, add a line in your PR's description that says `Closes #123`, where `#123` is the number of the issue you're fixing. > Pull requests (or PRs for short) are the primary mechanism we use to change modmail. GitHub itself has some [great documentation][about-pull-requests] on using the Pull Request feature. We use the "fork and pull" model [described here][development-models], where contributors push changes to their personal fork and create pull requests to bring those changes into the source repository. -[about-pull-requests]: https://help.github.com/articles/about-pull-requests/ -[development-models]: https://help.github.com/articles/about-collaborative-development-models/ - Once you've filed the PR: -* Barring special circumstances, maintainers will not review PRs until lint checks pass (`poetry run task lint`). -* One or more contributors will use GitHub's review feature to review your PR. -* If the maintainer asks for any changes, edit your changes, push, and ask for another review. -* If the maintainer decides to pass on your PR, they will thank you for the contribution and explain why they won't be accepting the changes. That's ok! We still really appreciate you taking the time to do it, and we don't take that lightly. πŸ’š -* If your PR gets accepted, it will be marked as such, and merged into the `main` branch soon after. - +- Barring special circumstances, maintainers will not review PRs until lint checks pass (`poetry run task lint`). +- One or more contributors will use GitHub's review feature to review your PR. +- If the maintainer asks for any changes, edit your changes, push, and ask for another review. +- If the maintainer decides to pass on your PR, they will thank you for the contribution and explain why they won't be accepting the changes. That's ok! We still really appreciate you taking the time to do it, and we don't take that lightly. πŸ’š +- If your PR gets accepted, it will be marked as such, and merged into the `main` branch soon after. ## Git Commit Messages @@ -177,7 +180,7 @@ written in the imperative, followed by an optional, more detailed explanatory text which is separated from the summary by an empty line. Commit messages should follow best practices, including explaining the context -of the problem and how it was solved, including in caveats or follow up changes +of the problem and how it was solved, including caveats or follow up changes required. They should tell the story of the change and provide readers understanding of what led to it. @@ -203,8 +206,13 @@ Remember, you're telling part of the story with the commit message. Don't make your chapter weird. ## Python Styleguide + WIP... ## Attribution This contributing guide is inspired by the [Moby's](https://github.com/moby/moby) and [Atom Text Editor's](https://github.com/atom/atom) contributing guide. + +[about-pull-requests]: https://help.github.com/articles/about-pull-requests/ +[development-models]: https://help.github.com/articles/about-collaborative-development-models/ +[fork]: https://github.com/discord-modmail/modmail/fork diff --git a/README.md b/README.md index 72b83623..41a0ec1f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # modmail + [![Lint & Test](https://img.shields.io/github/workflow/status/discord-modmail/modmail/Lint%20&%20Test/main?label=Lint+%26+Test&logo=github&style=flat)](https://github.com/discord-modmail/modmail/actions/workflows/lint_test.yml "Lint and Test") [![Code Coverage](https://img.shields.io/codecov/c/gh/discord-modmail/modmail/main?logo=codecov&style=flat&label=Code+Coverage)](https://app.codecov.io/gh/discord-modmail/modmail "Code Coverage") [![Codacy Grade](https://img.shields.io/codacy/grade/78be21a49835484595aea556d5920638?logo=codacy&style=flat&label=Code+Quality)](https://www.codacy.com/gh/discord-modmail/modmail/dashboard "Codacy Grade") -[![Python](https://img.shields.io/static/v1?label=Python&message=3.8+|+3.9&color=blue&logo=Python&style=flat)](https://www.python.org/downloads/ "Python 3.8 | 3.9") +[![Python](https://img.shields.io/static/v1?label=Python&message=3.8+%7C+3.9&color=blue&logo=Python&style=flat)](https://www.python.org/downloads/ "Python 3.8 | 3.9") [![License](https://img.shields.io/github/license/discord-modmail/modmail?style=flat&label=License)](./LICENSE "License file") [![Code Style](https://img.shields.io/static/v1?label=Code%20Style&message=black&color=000000&style=flat)](https://github.com/psf/black "The uncompromising python formatter") diff --git a/SECURITY.md b/SECURITY.md index 0413dc18..4084290a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,7 +8,7 @@ versions long, as that may make contributing difficult. | Version | Supported | | ------- | ------------------ | -| < 1.0 | :white_check_mark: | +| \< 1.0 | :white_check_mark: | | >= 1.0 | Unreleased | ## Reporting a Vulnerability From 46ae054ed44e6be8a26e167f783300f79c1f4d23 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 17:13:15 -0400 Subject: [PATCH 132/169] docs: add a changelog based on Keep a Changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f2445517 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Bot modes to determine behavior. + - `PROD`, `DEVELOP`, `PLUGIN_DEV` +- Extension loading system + - scans for extensions in the `modmail/extensions` folder and loads them if they are of the right format. +- Plugin loading system + - scans for plugins in the plugins folder and loads them. +- Extension management commands + - Run the `ext` command for more details when bot is in `DEVELOP` mode. +- Plugin management commands +- - Run the `plugins` command for more details. +- Extension metadata + - used to determine if a cog should load or not depending on the bot mode +- Guide on how to contribute to modmail, see [CONTRIBUTING.md](./CONTRIBUTING.md) + +[unreleased]: https://github.com/discord-modmail/modmail/compare/main From f779b7f972f888a1bfb44e821c16efbd9bf87daa Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 19:26:42 -0400 Subject: [PATCH 133/169] docs: remove extra words --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea1a7ccb..29726f1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -136,7 +136,7 @@ poetry run task precommit | False | `DEVELOP` | Boolean | Enables the developer bot extensions | | False | `PLUGIN_DEV` | Boolean | Enables plugin-developer friendly bot extensions | -The rest of them can be viewed in our example file. [.env.template](./.env.template) +The rest of them can be viewed in our [.env.template](./.env.template) ### Run The Project From 852ace205578a14002505df103973f02be02267f Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 19:27:13 -0400 Subject: [PATCH 134/169] docs: use sh instead of shell for codeblocks --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 29726f1f..2d823d6b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,7 +68,7 @@ Once it's filed: ## Project Setup -So you wanna contribute some code! That's great! This project uses GitHub Pull Requests to manage contributions, so [read up on how to fork a GitHub project and file a PR](https://guides.github.com/activities/forking) if you've never done it before. +So you want to contribute some code! That's great! This project uses GitHub Pull Requests to manage contributions, so [read up on how to fork a GitHub project and file a PR](https://guides.github.com/activities/forking) if you've never done it before. ### Test Server and Bot Account @@ -91,7 +91,7 @@ And you should be ready to go! Once you have your fork you will need to clone the repo to your computer. -```shell +```sh $ git clone https://github.com//modmail ... $ cd modmail @@ -99,7 +99,7 @@ $ cd modmail or using the [github cli](https://github.com/cli/cli): -```shell +```sh $ gh repo clone /modmail ... $ cd modmail @@ -111,7 +111,7 @@ $ cd modmail Make sure you are in the project directory. -```shell +```sh # This will install the development and project dependencies. poetry install # This will install the pre-commit hooks. From 5ed363297e66ec44eb87bd305dc755f66349a666 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 21:10:46 -0400 Subject: [PATCH 135/169] feat: add cmd to refresh plugins and exts add a way to dynamically refresh the plugins and extension lists while the bot is running --- modmail/extensions/extension_manager.py | 15 ++++++++++++++- modmail/extensions/plugin_manager.py | 20 +++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 4d26967a..d416389c 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -14,7 +14,7 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger from modmail.utils.cogs import BotModes, ExtMetadata, ModmailCog -from modmail.utils.extensions import EXTENSIONS, NO_UNLOAD, unqualify +from modmail.utils.extensions import EXTENSIONS, NO_UNLOAD, unqualify, walk_extensions log: ModmailLogger = logging.getLogger(__name__) @@ -88,6 +88,7 @@ class ExtensionManager(ModmailCog, name="Extension Manager"): def __init__(self, bot: ModmailBot): self.bot = bot self.all_extensions = EXTENSIONS + self.refresh_method = walk_extensions def get_black_listed_extensions(self) -> list: """Returns a list of all unload blacklisted extensions.""" @@ -187,6 +188,18 @@ async def list_extensions(self, ctx: Context) -> None: # TODO: since we currently don't have a paginator. await ctx.send("".join(lines) or f"There are no {self.type}s installed.") + @extensions_group.command(name="refresh", aliases=("rewalk",)) + async def rewalk_extensions(self, ctx: Context) -> None: + """ + Refreshes the list of extensions. + + Typical use case is in the event that the existing extensions have changed while the bot is running. + """ + log.debug(f"Refreshing list of {self.type}s.") + self.all_extensions.clear() + self.all_extensions.update(self.refresh_method()) + await ctx.send(f":ok_hand: Refreshed list of {self.type}s.") + def group_extension_statuses(self) -> t.Mapping[str, str]: """Return a mapping of extension names and statuses to their categories.""" categories = defaultdict(list) diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 341e5516..9fc1f47b 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -4,7 +4,7 @@ from modmail.bot import ModmailBot from modmail.extensions.extension_manager import ExtensionConverter, ExtensionManager from modmail.utils.cogs import BotModes, ExtMetadata -from modmail.utils.plugins import PLUGINS +from modmail.utils.plugins import PLUGINS, walk_plugins EXT_METADATA = ExtMetadata(load_if_mode=BotModes.PRODUCTION) @@ -29,6 +29,7 @@ class PluginManager(ExtensionManager, name="Plugin Manager"): def __init__(self, bot: ModmailBot) -> None: super().__init__(bot) self.all_extensions = PLUGINS + self.refresh_method = walk_plugins def get_black_listed_extensions(self) -> list: """ @@ -65,24 +66,29 @@ async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: @plugins_group.command(name="reload", aliases=("r", "rl")) async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: """ - Reload extensions given their fully qualified or unqualified names. + Reload plugins given their fully qualified or unqualified names. - If an extension fails to be reloaded, it will be rolled-back to the prior working state. + If an plugin fails to be reloaded, it will be rolled-back to the prior working state. - If '\*' or '\*\*' is given as the name, all currently loaded extensions will be reloaded. + If '\*' or '\*\*' is given as the name, all currently loaded plugins will be reloaded. """ # noqa: W605 await self.reload_extensions.callback(self, ctx, *plugins) @plugins_group.command(name="list", aliases=("all", "ls")) async def list_plugins(self, ctx: Context) -> None: """ - Get a list of all extensions, including their loaded status. + Get a list of all plugins, including their loaded status. - Grey indicates that the extension is unloaded. - Green indicates that the extension is currently loaded. + Red indicates that the plugin is unloaded. + Green indicates that the plugin is currently loaded. """ await self.list_extensions.callback(self, ctx) + @plugins_group.command(name="refresh", aliases=("rewalk",)) + async def rewalk_plugins(self, ctx: Context) -> None: + """Refreshes the list of installed plugins.""" + await self.rewalk_extensions.callback(self, ctx) + # TODO: Implement install/enable/disable/etc # This cannot be static (must have a __func__ attribute). From 1fa18096ad1062ca7ad12e4a8bd5175b516346b6 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 21:53:38 -0400 Subject: [PATCH 136/169] fix: create an http_session on bot start --- CHANGELOG.md | 4 ++++ modmail/bot.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2445517..1624cbeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,4 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - used to determine if a cog should load or not depending on the bot mode - Guide on how to contribute to modmail, see [CONTRIBUTING.md](./CONTRIBUTING.md) +### Fixed + +- Make the bot http_session within an event loop. + [unreleased]: https://github.com/discord-modmail/modmail/compare/main diff --git a/modmail/bot.py b/modmail/bot.py index db0758db..deb66464 100644 --- a/modmail/bot.py +++ b/modmail/bot.py @@ -32,6 +32,15 @@ async def create_session(self) -> None: """Create an aiohttp client session.""" self.http_session = ClientSession() + async def start(self, *args, **kwargs) -> None: + """ + Start the bot. + + This just serves to create the http session. + """ + await self.create_session() + await super().start(*args, **kwargs) + async def close(self) -> None: """Safely close HTTP session, unload plugins and extensions when the bot is shutting down.""" plugins = self.extensions & PLUGINS.keys() @@ -84,7 +93,7 @@ def load_plugins(self) -> None: # any errors here will take down the entire bot self.load_extension(plugin) except Exception: - self.logger.error("Failed to load plugin {0}".format(plugin)) + self.logger.error("Failed to load plugin {0}".format(plugin), exc_info=True) def add_cog(self, cog: commands.Cog, *, override: bool = False) -> None: """ From cf740516f189767b54db4fcf34b397971ec46b82 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 22:05:35 -0400 Subject: [PATCH 137/169] fix: proper error message for unloaded exts --- modmail/extensions/extension_manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index d416389c..f26e3eb6 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -253,7 +253,11 @@ def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: try: action.value(self.bot, ext) except (commands.ExtensionAlreadyLoaded, commands.ExtensionNotLoaded): - msg = f":x: {self.type.capitalize()} `{ext}` is already {verb}ed." + if action is Action.RELOAD: + # When reloading, have a special error. + msg = f":x: {self.type.capitalize()} `{ext}` is not loaded, so it was not {verb}ed." + else: + msg = f":x: {self.type.capitalize()} `{ext}` is already {verb}ed." except Exception as e: if hasattr(e, "original"): e = e.original From 0463efa3aac2c3a735207d3ec3c26688f6447809 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 22:24:22 -0400 Subject: [PATCH 138/169] chore: increase kept bot logs --- modmail/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index 680be253..c87eca1a 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -26,8 +26,8 @@ # file handler file_handler = logging.handlers.RotatingFileHandler( log_file, - maxBytes=5 * (2 ** 12), - backupCount=5, + maxBytes=5 * (2 ** 14), + backupCount=7, encoding="utf-8", ) From 5f3a74ff57366b8141e26854e56a150b8ef2d652 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 10 Aug 2021 23:04:52 -0400 Subject: [PATCH 139/169] docs: prepare changelog for 0.1.0 --- CHANGELOG.md | 5 ++++- pyproject.toml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1624cbeb..ff71cdce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.0] + ### Added - Bot modes to determine behavior. @@ -27,4 +29,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Make the bot http_session within an event loop. -[unreleased]: https://github.com/discord-modmail/modmail/compare/main +[0.1.0]: https://github.com/discord-modmail/modmail/releases/tag/v0.1.0 +[unreleased]: https://github.com/discord-modmail/modmail/compare/v0.1.0...main diff --git a/pyproject.toml b/pyproject.toml index 4d42ecab..c075fe71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ repository = "https://github.com/discord-modmail/modmail" homepage = "https://github.com/discord-modmail/modmail" keywords = ["discord", "modmail"] +include = ["CHANGELOG.md"] packages = [{ include = "modmail" }] [tool.poetry.dependencies] From 3d56fd9fdb140f1232f64d2c4eda8fb5c9adabe9 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 01:26:34 -0400 Subject: [PATCH 140/169] chore: add dotenv to pydantic extras --- poetry.lock | 9 +++++---- pyproject.toml | 2 +- requirements.txt | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index b4a03026..1de39091 100644 --- a/poetry.lock +++ b/poetry.lock @@ -255,7 +255,7 @@ toml = ["toml"] [[package]] name = "discord.py" -version = "2.0.0a3453+g58ca9e99" +version = "2.0.0a3466+g6e6c8a7b" description = "A Python wrapper for the Discord API" category = "main" optional = false @@ -274,7 +274,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] type = "git" url = "https://github.com/Rapptz/discord.py.git" reference = "master" -resolved_reference = "58ca9e99325ac2678a51cd63afd7ae917f14bc8d" +resolved_reference = "6e6c8a7b2810747222a938c7fe3e466c2994b23f" [[package]] name = "distlib" @@ -711,6 +711,7 @@ optional = false python-versions = ">=3.6.1" [package.dependencies] +python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""} typing-extensions = ">=3.7.4.3" [package.extras] @@ -874,7 +875,7 @@ name = "python-dotenv" version = "0.19.0" description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" -optional = true +optional = false python-versions = ">=3.5" [package.extras] @@ -1050,7 +1051,7 @@ environs = ["environs"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "4390ff17f47b1772048e805984a4466b3420c4dbaa37b454a94a2725ff595327" +content-hash = "ffc92d956a0a41f4495c9f72484a26d0d261836d15b984d686d8e44276bd6244" [metadata.files] aiodns = [ diff --git a/pyproject.toml b/pyproject.toml index c075fe71..c1140a9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ colorama = "^0.4.3" coloredlogs = "^15.0" "discord.py" = {git = "https://github.com/Rapptz/discord.py.git", rev = "master"} environs = {version = "~=9.3.3", optional = true} -pydantic = "^1.8.2" +pydantic = {version = "^1.8.2", extras = ["dotenv"]} toml = "^0.10.2" [tool.poetry.extras] diff --git a/requirements.txt b/requirements.txt index f74b0a83..b53d19e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ pycparser==2.20; python_version >= "3.6" and python_full_version < "3.0.0" or py pydantic==1.8.2; python_full_version >= "3.6.1" pyreadline==2.1; python_version >= "2.7" and python_full_version < "3.0.0" and sys_platform == "win32" or python_full_version >= "3.5.0" and sys_platform == "win32" python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" +python-dotenv==0.19.0; python_full_version >= "3.6.1" and python_version >= "3.5" six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") typing-extensions==3.10.0.0; python_version >= "3.6" or python_full_version >= "3.6.1" or python_version >= "3.6" and python_full_version >= "3.8.0" From b781cf6e1dd2a7cdbc63bac688853b5b61594509 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 02:05:18 -0400 Subject: [PATCH 141/169] chore: remove unused vars from .env.template --- .env.template | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.env.template b/.env.template index dcdac082..79023bad 100644 --- a/.env.template +++ b/.env.template @@ -1,13 +1 @@ TOKEN="MyBotToken" -GUILD_ID=1234567890 -OWNERS="hello world" - -MODMAIL_GUILD_ID=1234567890 - -# DATABASE -CONNECTION_URI="mongodb+srv://mongodburi" - -# DISCORD LOGIN -CLIENT_ID = "YOUR APP'S CLIENT ID HERE" -CLIENT_SECRET = "YOUR APP'S CLIENT SECRET HERE" -REDIRECT_URI = "https://yourdomain.com/callback" From 7c91e97a11e29f400bc5720df6434b8393241c7e Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 02:34:06 -0400 Subject: [PATCH 142/169] chore: update discord.py --- poetry.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1de39091..520ef224 100644 --- a/poetry.lock +++ b/poetry.lock @@ -255,7 +255,7 @@ toml = ["toml"] [[package]] name = "discord.py" -version = "2.0.0a3466+g6e6c8a7b" +version = "2.0.0a3467+g08a4db39" description = "A Python wrapper for the Discord API" category = "main" optional = false @@ -274,7 +274,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] type = "git" url = "https://github.com/Rapptz/discord.py.git" reference = "master" -resolved_reference = "6e6c8a7b2810747222a938c7fe3e466c2994b23f" +resolved_reference = "08a4db396118aeda6205ff56c8c8fc565fc338fc" [[package]] name = "distlib" @@ -1016,7 +1016,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.7.1" +version = "20.7.2" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1749,8 +1749,8 @@ urllib3 = [ {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] virtualenv = [ - {file = "virtualenv-20.7.1-py2.py3-none-any.whl", hash = "sha256:73863dc3be1efe6ee638e77495c0c195a6384ae7b15c561f3ceb2698ae7267c1"}, - {file = "virtualenv-20.7.1.tar.gz", hash = "sha256:57bcb59c5898818bd555b1e0cfcf668bd6204bc2b53ad0e70a52413bd790f9e4"}, + {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"}, + {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"}, ] yarl = [ {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, From 0b3c055e032c93bb7847b587a8351eaa2f59b52d Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 04:05:52 -0400 Subject: [PATCH 143/169] docs: comment todo in readme --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d823d6b..0a5a2269 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -207,7 +207,7 @@ your chapter weird. ## Python Styleguide -WIP... + ## Attribution From 42bb1d72f5f8a6ba4f2a5fb393c987f352181991 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 04:08:07 -0400 Subject: [PATCH 144/169] docs: mention rejected features can be plugins --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a5a2269..15d2f5d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,8 @@ Thank you so much for your interest in contributing!. All types of contributions Please make sure to read the relevant section before making your contribution! It will make it a lot easier for us maintainers to make the most of it and smooth out the experience for all involved. πŸ’š > **NOTE**: failing to comply with our guidelines may lead to a rejection of the contribution. +> However, most features that are rejected are able to be written as a plugin, and used on your +> bot, without blocking you from getting updates. If you are confused by any of these rules, feel free to ask us in the `#support` channel in our [Discord server.](https://discord.gg/ERteGkedDW) From 393fc5cb0008ae877488c126186f25245af042dc Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 04:20:22 -0400 Subject: [PATCH 145/169] chore: add all classes plugins should use to plug helpers --- modmail/plugin_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/plugin_helpers.py b/modmail/plugin_helpers.py index aa9a9be6..d20abf8e 100644 --- a/modmail/plugin_helpers.py +++ b/modmail/plugin_helpers.py @@ -1,8 +1,8 @@ from modmail.bot import ModmailBot from modmail.log import ModmailLogger -from modmail.utils.cogs import ModmailCog +from modmail.utils.cogs import BotModes, ExtMetadata, ModmailCog -__all__ = ["PluginCog", ModmailBot, ModmailLogger] +__all__ = ["PluginCog", ModmailBot, ModmailLogger, BotModes, ExtMetadata] class PluginCog(ModmailCog): From d66f96626bb41c27808dd3aac27dd3bb479ec20b Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 04:25:23 -0400 Subject: [PATCH 146/169] fix: plugin metadata doesn't use missing method metadata was previously refactored, but the code in walk_plugins was not updated to use the new method, and no plugin had metadata to test with --- modmail/utils/extensions.py | 2 +- modmail/utils/plugins.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 6c74fc88..661f157d 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -64,7 +64,7 @@ def on_error(name: str) -> t.NoReturn: # If it lacks a setup function, it's not an extension. continue - ext_metadata: "ExtMetadata" = getattr(imported, "EXT_METADATA", None) + ext_metadata: ExtMetadata = getattr(imported, "EXT_METADATA", None) if ext_metadata is not None: # check if this cog is dev only or plugin dev only load_cog = bool(int(ext_metadata.load_if_mode) & BOT_MODE) diff --git a/modmail/utils/plugins.py b/modmail/utils/plugins.py index 2dc7abc8..b7b1a236 100644 --- a/modmail/utils/plugins.py +++ b/modmail/utils/plugins.py @@ -65,7 +65,7 @@ def walk_plugins() -> t.Iterator[t.Tuple[str, bool]]: ext_metadata: ExtMetadata = getattr(imported, "EXT_METADATA", None) if ext_metadata is not None: # check if this plugin is dev only or plugin dev only - load_cog = (ext_metadata.load_if_mode & BOT_MODE).to_strings() + load_cog = bool(int(ext_metadata.load_if_mode) & BOT_MODE) log.trace(f"Load plugin {imported.__name__!r}?: {load_cog}") yield imported.__name__, load_cog continue From b767f2312b56dd44b19e73e9982f322eae0d26a9 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 04:30:59 -0400 Subject: [PATCH 147/169] docs: add missing changes to changelog --- CHANGELOG.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff71cdce..8508e3e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,19 +11,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Bot modes to determine behavior. - - `PROD`, `DEVELOP`, `PLUGIN_DEV` +- Bot modes to determine behavior. Multiple can be applied at once. + - `PROD`: the default mode, no dev extensions or dev plugins load + - `DEVELOP`: the bot developer mode, most useful for people adding features to modmail + - Enables the extension_manager extension. + - `PLUGIN_DEV`: the plugin developer mode, useful for enabling plugin specific features + - This is not used yet. - Extension loading system - scans for extensions in the `modmail/extensions` folder and loads them if they are of the right format. - Plugin loading system - - scans for plugins in the plugins folder and loads them. + - scans for plugins in the `modmail/plugins` folder and loads them. - Extension management commands + - load, reload, unload, list, refresh commands for dealing with extensions - Run the `ext` command for more details when bot is in `DEVELOP` mode. - Plugin management commands + - load, reload, unload, list, refresh commands for dealing with plugins - - Run the `plugins` command for more details. - Extension metadata - used to determine if a cog should load or not depending on the bot mode +- Plugin helper file + - `modmail/plugin_helpers.py` contains several helpers for making plugins + - `PluginCog` + - `ModmailBot`, imported from `modmail.bot` + - `ModmailLogger`, imported from `modmail.log` +- Meta Cog + - **NOTE**: The commands in this cog are not stabilized yet and should not be relied upon. + - Prefix command for getting the set prefix. Most useful by mentioning the bot. + - Uptime command which tells the end user how long the bot has been online. + - Ping command to see the bot latency. - Guide on how to contribute to modmail, see [CONTRIBUTING.md](./CONTRIBUTING.md) +- Start a Changelog ### Fixed From d4222b288e4a3256033c82c3b6b5b047d135f3a2 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Wed, 11 Aug 2021 16:13:00 +0530 Subject: [PATCH 148/169] chore: Make contrib guide less wordy --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15d2f5d3..43916b7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,8 +5,8 @@ Thank you so much for your interest in contributing!. All types of contributions Please make sure to read the relevant section before making your contribution! It will make it a lot easier for us maintainers to make the most of it and smooth out the experience for all involved. πŸ’š > **NOTE**: failing to comply with our guidelines may lead to a rejection of the contribution. -> However, most features that are rejected are able to be written as a plugin, and used on your -> bot, without blocking you from getting updates. +> However, most features that are rejected can be written as a plugin, and used on your +> modmail, without blocking you from getting updates. If you are confused by any of these rules, feel free to ask us in the `#support` channel in our [Discord server.](https://discord.gg/ERteGkedDW) @@ -42,11 +42,11 @@ If you run into an error or bug with the project: - Open an Issue at [discord-modmail/modmail/issues](https://github.com/discord-modmail/modmail/issues) - Explain the problem and include additional details to help maintainers reproduce the problem: - **Use a clear and descriptive title** for the issue to identify the problem. - - **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. + - **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did but explain how you did it**. - **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/paste-able snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). - **Describe the behaviour you observed after following the steps** and point out what exactly is the problem with that behaviour. - **Explain which behaviour you expected to see instead and why.** - - **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) on Linux (ofcourse there are plenty more). + - **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) on Linux (of course there are plenty more). ## Request a Feature @@ -64,7 +64,7 @@ Once it's filed: - The Maintainers will [label the issue](#label-issues). - The Maintainers will evaluate the feature request, possibly asking you more questions to understand its purpose and any relevant requirements. If the issue is closed, the team will convey their reasoning and suggest an alternative path forward. -- If the feature request is accepted, it will be marked for implementation with `status: approved`, which can then be done by either by a core team member or by anyone in the community who wants to contribute code. +- If the feature request is accepted, it will be marked for implementation with `status: approved`, which can then be done either by a core team member or by anyone in the community who wants to contribute code. > **Note**: The team is unlikely to be able to accept every single feature request that is filed. Please understand if they need to say no. From 5d8db2c8189c7574ab92b701f24bed18ae615e41 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Wed, 11 Aug 2021 16:34:15 +0530 Subject: [PATCH 149/169] CI: Add CI to check for PR numbers on changelog (do necessary changes) --- .github/workflows/changelog.yml | 21 +++++++++++++++++ CHANGELOG.md | 28 ++++++++-------------- pyproject.toml | 41 +++++++++++++-------------------- 3 files changed, 46 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/changelog.yml diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 00000000..562311d7 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,21 @@ +# Github Action Workflow enforcing our changelog style of enforcing PR number. + +name: Changelog Entry Check + +on: + pull_request: + types: [opened, synchronize, labeled, unlabeled, reopened] + +jobs: + build: + name: Changelog Entry Check + runs-on: ubuntu-latest + + steps: + - name: Grep CHANGES.md for PR number + - uses: actions/checkout@v2 + if: contains(github.event.pull_request.labels.*.name, 'skip changelog') != true + run: | + grep -Pz "\((\n\s*)?#${{ github.event.pull_request.number }}(\n\s*)?\)" CHANGES.md || \ + (echo "Please add '(#${{ github.event.pull_request.number }})' change line to CHANGES.md" && \ + exit 1) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8508e3e3..75ab6815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,50 +1,40 @@ # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - ## [0.1.0] ### Added -- Bot modes to determine behavior. Multiple can be applied at once. +- Bot modes to determine behavior. Multiple can be applied at once. (#43) - `PROD`: the default mode, no dev extensions or dev plugins load - `DEVELOP`: the bot developer mode, most useful for people adding features to modmail - Enables the extension_manager extension. - `PLUGIN_DEV`: the plugin developer mode, useful for enabling plugin specific features - This is not used yet. -- Extension loading system +- Extension loading system (#43) - scans for extensions in the `modmail/extensions` folder and loads them if they are of the right format. -- Plugin loading system +- Plugin loading system (#43) - scans for plugins in the `modmail/plugins` folder and loads them. -- Extension management commands +- Extension management commands (#43) - load, reload, unload, list, refresh commands for dealing with extensions - Run the `ext` command for more details when bot is in `DEVELOP` mode. -- Plugin management commands +- Plugin management commands (#43) - load, reload, unload, list, refresh commands for dealing with plugins -- - Run the `plugins` command for more details. -- Extension metadata + - Run the `plugins` command for more details. +- Extension metadata (#43) - used to determine if a cog should load or not depending on the bot mode -- Plugin helper file +- Plugin helper file (#43) - `modmail/plugin_helpers.py` contains several helpers for making plugins - `PluginCog` - `ModmailBot`, imported from `modmail.bot` - `ModmailLogger`, imported from `modmail.log` -- Meta Cog +- Meta Cog (#43) - **NOTE**: The commands in this cog are not stabilized yet and should not be relied upon. - Prefix command for getting the set prefix. Most useful by mentioning the bot. - Uptime command which tells the end user how long the bot has been online. - Ping command to see the bot latency. -- Guide on how to contribute to modmail, see [CONTRIBUTING.md](./CONTRIBUTING.md) -- Start a Changelog ### Fixed - Make the bot http_session within an event loop. [0.1.0]: https://github.com/discord-modmail/modmail/releases/tag/v0.1.0 -[unreleased]: https://github.com/discord-modmail/modmail/compare/v0.1.0...main diff --git a/pyproject.toml b/pyproject.toml index c1140a9a..19172e92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,16 +3,12 @@ name = "Modmail" version = "0.1.0" description = "A modmail bot for Discord. Python 3.8+ compatiable" license = "MIT" - authors = ["aru ", "bast "] - readme = "README.md" repository = "https://github.com/discord-modmail/modmail" homepage = "https://github.com/discord-modmail/modmail" keywords = ["discord", "modmail"] - include = ["CHANGELOG.md"] packages = [{ include = "modmail" }] @@ -34,9 +30,7 @@ environs = ["environs"] # always needed pre-commit = "~=2.1" taskipy = "^1.6.0" - -# linting, needed if intending to make a commit -# pre-commit requires flake8 +# linting black = "^21.7b0" flake8 = "~=3.8" flake8-annotations = "~=2.3" @@ -49,7 +43,6 @@ flake8-tidy-imports = "~=4.1" flake8-todo = "~=0.7" isort = "^5.9.2" pep8-naming = "~=0.11" - # testing codecov = "^2.1.11" coverage = { extras = ["toml"], version = "^5.5" } @@ -60,11 +53,25 @@ pytest-dependency = "^0.5.1" pytest-docs = "^0.1.0" pytest-xdist = { version = "^2.3.0", extras = ["psutil"] } - [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" +[tool.coverage.run] +branch = true +source_pkgs = ["modmail"] +omit = ["modmail/plugins/**.*"] + +[tool.pytest.ini_options] +addopts = "--cov -ra" +minversion = "6.0" +testpaths = ["tests"] + +[tool.black] +line-length = 110 +target-version = ['py38'] +include = '\.pyi?$' + [tool.taskipy.tasks.export] cmd = """ echo 'Exporting installed packages to requirements.txt.\n\ @@ -88,19 +95,3 @@ report = { cmd = "coverage report", help = "Show coverage report from previously start = { cmd = "python -m modmail", help = "Run bot" } test = { cmd = "pytest -n auto --dist loadfile --cov-report= --cov= --docs tests/docs.md", help = "Runs tests and save results to a coverage report" } test-nocov = { cmd = "pytest --no-cov", help = "Runs tests without creating a coverage report" } - - -[tool.coverage.run] -branch = true -source_pkgs = ["modmail"] -omit = ["modmail/plugins/**.*"] - -[tool.pytest.ini_options] -addopts = "--cov -ra" -minversion = "6.0" -testpaths = ["tests"] - -[tool.black] -line-length = 110 -target-version = ['py38'] -include = '\.pyi?$' From 51354343c626e00bf168822a00716decc7a7d781 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Wed, 11 Aug 2021 16:46:55 +0530 Subject: [PATCH 150/169] docs(contrib): Add changelog style guide --- .github/workflows/changelog.yml | 1 + CONTRIBUTING.md | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 562311d7..cb624ca1 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,4 +1,5 @@ # Github Action Workflow enforcing our changelog style of enforcing PR number. +# credit: https://github.com/psf/black/blob/main/.github/workflows/changelog.yml name: Changelog Entry Check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43916b7f..706927b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Please make sure to read the relevant section before making your contribution! I > However, most features that are rejected can be written as a plugin, and used on your > modmail, without blocking you from getting updates. -If you are confused by any of these rules, feel free to ask us in the `#support` channel in our [Discord server.](https://discord.gg/ERteGkedDW) +If you are confused by any of these rules, feel free to ask us in the `#support` channel in our [Discord server.][modmail-discord] ## How do I... @@ -22,12 +22,13 @@ If you are confused by any of these rules, feel free to ask us in the `#support` - Style Guides βœ…πŸ™†πŸΌπŸ’ƒπŸ‘” - [Git Commit Messages](#git-commit-messages) - [Python Styleguide](#python-styleguide) + - [Changelog Requirement](#changelog-requirement) ## Request Support -- You can either ask your question as issue by opening one at https://github.com/discord-modmail/modmail/issues. +- You can either ask your question as issue by opening one at [discord-modmail/modmail/issues][modmail-issues]. -- [Join the Modmail Discord Server](https://discord.gg/ERteGkedDW) +- [Join the Modmail Discord Server][modmail-discord] - Even though Discord is a chat service, sometimes it takes several hours for community members to respond β€” please be patient! - Use the `#support` channel for questions or discussion about writing or contributing to Discord Modmail bot. @@ -39,7 +40,7 @@ If you run into an error or bug with the project: > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. -- Open an Issue at [discord-modmail/modmail/issues](https://github.com/discord-modmail/modmail/issues) +- Open an Issue at [discord-modmail/modmail/issues][modmail-issues]. - Explain the problem and include additional details to help maintainers reproduce the problem: - **Use a clear and descriptive title** for the issue to identify the problem. - **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did but explain how you did it**. @@ -52,7 +53,7 @@ If you run into an error or bug with the project: If the project doesn't do something you need or want it to do: -- Open an Issue at https://github.com/discord-modmail/modmail/issues +- Open an Issue at [discord-modmail/modmail/issues][modmail-issues]. - Provide as much context as you can about what you're running into. - **Use a clear and descriptive title** for the issue to identify the suggestion. - **Provide a step-by-step description of the suggested enhancement** in as many details as possible. @@ -161,7 +162,7 @@ To contribute code: - Write clear, concise commit message(s). - A more in-depth guide to writing great commit messages can be found in Chris Beam's [*How to Write a Git Commit Message*](https://chris.beams.io/posts/git-commit/). - Run `flake8`, `black` and `pre-commit` against your code **before** you push. Your commit will be rejected by the build server if it fails to lint. You can run the lint by executing `poetry run task lint` in your command line. -- Go to [discord-modmail/modmail/pulls](https://github.com/discord-modmail/modmail/pulls) and open a new pull request with your changes. +- Go to [discord-modmail/modmail/pulls][modmail-pulls] and open a new pull request with your changes. - If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This permits maintainers to commit changes directly to your fork, speeding up the review process. - If your PR is connected to an open issue, add a line in your PR's description that says `Closes #123`, where `#123` is the number of the issue you're fixing. @@ -211,6 +212,21 @@ your chapter weird. +## Changelog Requirement + +Modmail has CI that will check for an entry corresponding to your PR in `CHANGES.md`. +If you feel this PR does not require a changelog entry please state that in a comment +and a maintainer can add a `skip changelog` label to make the CI pass. Otherwise, +please ensure you have a line in the following format: + +``` +- `Modmail` is now more awesome (#X) +``` + +Note that X should be your PR number, not issue number! This is not perfect but +saves a lot of release overhead as now the releaser does not need to go back and +workout what to add to the `CHANGES.md` for each release. + ## Attribution This contributing guide is inspired by the [Moby's](https://github.com/moby/moby) and [Atom Text Editor's](https://github.com/atom/atom) contributing guide. @@ -218,3 +234,6 @@ This contributing guide is inspired by the [Moby's](https://github.com/moby/moby [about-pull-requests]: https://help.github.com/articles/about-pull-requests/ [development-models]: https://help.github.com/articles/about-collaborative-development-models/ [fork]: https://github.com/discord-modmail/modmail/fork +[modmail-discord]: https://discord.gg/ERteGkedDW +[modmail-issues]: https://github.com/discord-modmail/modmail/issues +[modmail-pulls]: https://github.com/discord-modmail/modmail/pulls From e7a2655190376698a2326d127700b863b85dfc7c Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Wed, 11 Aug 2021 16:54:23 +0530 Subject: [PATCH 151/169] CI: `uses` is supposed to be before defining step --- .github/workflows/changelog.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index cb624ca1..473418ca 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Grep CHANGES.md for PR number - uses: actions/checkout@v2 + - name: Grep CHANGES.md for PR number if: contains(github.event.pull_request.labels.*.name, 'skip changelog') != true run: | - grep -Pz "\((\n\s*)?#${{ github.event.pull_request.number }}(\n\s*)?\)" CHANGES.md || \ - (echo "Please add '(#${{ github.event.pull_request.number }})' change line to CHANGES.md" && \ + grep -Pz "\((\n\s*)?#${{ github.event.pull_request.number }}(\n\s*)?\)" CHANGELOG.md || \ + (echo "Please add '(#${{ github.event.pull_request.number }})' change line to CHANGELOG.md" && \ exit 1) From 3dff2c5eeb0cadd5a75befc49b9a98bfa824477c Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 13:00:07 -0400 Subject: [PATCH 152/169] changelog: readd credit to Keep a Changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ab6815..a79da443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + ## [0.1.0] ### Added @@ -32,9 +39,12 @@ - Prefix command for getting the set prefix. Most useful by mentioning the bot. - Uptime command which tells the end user how long the bot has been online. - Ping command to see the bot latency. +- Guide on how to contribute to modmail, see [CONTRIBUTING.md](./CONTRIBUTING.md) +- Start a Changelog ### Fixed - Make the bot http_session within an event loop. [0.1.0]: https://github.com/discord-modmail/modmail/releases/tag/v0.1.0 +[unreleased]: https://github.com/discord-modmail/modmail/compare/v0.1.0...main From 3540814e77019ad62bcd848c97cc5f8da76d8467 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 13:37:42 -0400 Subject: [PATCH 153/169] chore: add .env.template to the build --- pyproject.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19172e92..c1122012 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" repository = "https://github.com/discord-modmail/modmail" homepage = "https://github.com/discord-modmail/modmail" keywords = ["discord", "modmail"] -include = ["CHANGELOG.md"] +include = ["CHANGELOG.md", "env.template"] packages = [{ include = "modmail" }] [tool.poetry.dependencies] @@ -18,9 +18,9 @@ aiohttp = { extras = ["speedups"], version = "^3.7.4" } arrow = "^1.1.1" colorama = "^0.4.3" coloredlogs = "^15.0" -"discord.py" = {git = "https://github.com/Rapptz/discord.py.git", rev = "master"} -environs = {version = "~=9.3.3", optional = true} -pydantic = {version = "^1.8.2", extras = ["dotenv"]} +"discord.py" = { git = "https://github.com/Rapptz/discord.py.git", rev = "master" } +environs = { version = "~=9.3.3", optional = true } +pydantic = { version = "^1.8.2", extras = ["dotenv"] } toml = "^0.10.2" [tool.poetry.extras] @@ -86,11 +86,11 @@ help = "Export installed packages in requirements.txt format" [tool.taskipy.tasks] black = { cmd = "black --check .", help = "dry run of black" } -codecov-validate = { cmd= "curl --data-binary @.codecov.yml https://codecov.io/validate", help = "Validate `.codecov.yml` with their api." } +codecov-validate = { cmd = "curl --data-binary @.codecov.yml https://codecov.io/validate", help = "Validate `.codecov.yml` with their api." } flake8 = { cmd = "python -m flake8", help = "Lints code with flake8" } lint = { cmd = "pre-commit run --all-files", help = "Checks all files for CI errors" } precommit = { cmd = "pre-commit install --install-hooks", help = "Installs the precommit hook" } -pytest-docs = { cmd = "pytest --no-cov --docs tests/docs.md", help = "Create docs for tests using pytest-docs."} +pytest-docs = { cmd = "pytest --no-cov --docs tests/docs.md", help = "Create docs for tests using pytest-docs." } report = { cmd = "coverage report", help = "Show coverage report from previously run tests." } start = { cmd = "python -m modmail", help = "Run bot" } test = { cmd = "pytest -n auto --dist loadfile --cov-report= --cov= --docs tests/docs.md", help = "Runs tests and save results to a coverage report" } From a7f99a0107b270cd35215a8cec030d1bd189c7e6 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 17:34:23 -0400 Subject: [PATCH 154/169] fix: ensure extensions can unload after renamed --- modmail/extensions/extension_manager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index f26e3eb6..940c17b0 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -196,7 +196,11 @@ async def rewalk_extensions(self, ctx: Context) -> None: Typical use case is in the event that the existing extensions have changed while the bot is running. """ log.debug(f"Refreshing list of {self.type}s.") + + # make sure the new walk contains all currently loaded extensions, so they can be unloaded + loaded_extensions = self.bot.extensions & self.all_extensions.keys() self.all_extensions.clear() + self.all_extensions.update(loaded_extensions) self.all_extensions.update(self.refresh_method()) await ctx.send(f":ok_hand: Refreshed list of {self.type}s.") From 433e55b83e0758d0f7c652b34043e9f44a79d6ce Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 18:09:35 -0400 Subject: [PATCH 155/169] tools: ensure mdformat only runs on md files --- .pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ddfe248..fb152f56 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,8 @@ repos: rev: 0.7.8 hooks: - id: mdformat - exclude: "tests/docs.md" + files: '\.md$' + exclude: 'tests/docs.md' additional_dependencies: - mdformat-gfm - mdformat-black From 91e3d5ba4e34447ba77001ff8edb4bbd62eaf0d6 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 18:10:14 -0400 Subject: [PATCH 156/169] fix: make sure plugins can be loaded from syslinks - modify the plugins walk to ensure that it scans symlinked folders for development, plugins will most likely be symlinked from a folder where they are developed, part of their git repo, and linked into the bot's plugin folder. --- modmail/utils/plugins.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/modmail/utils/plugins.py b/modmail/utils/plugins.py index b7b1a236..21c4af57 100644 --- a/modmail/utils/plugins.py +++ b/modmail/utils/plugins.py @@ -8,6 +8,7 @@ """ +import glob import importlib import importlib.util import inspect @@ -20,21 +21,28 @@ from modmail.utils.cogs import ExtMetadata from modmail.utils.extensions import BOT_MODE, unqualify -BASE_PATH = Path(plugins.__file__).parent - log: ModmailLogger = logging.getLogger(__name__) + +BASE_PATH = Path(plugins.__file__).parent.resolve() +PLUGIN_MODULE = "modmail.plugins" PLUGINS: t.Dict[str, t.Tuple[bool, bool]] = dict() def walk_plugins() -> t.Iterator[t.Tuple[str, bool]]: """Yield plugin names from the modmail.plugins subpackage.""" - for path in BASE_PATH.glob("**/*.py"): - # calculate the module name, if it were to have a name from the path - relative_path = path.relative_to(BASE_PATH) + # walk all files in the plugins folder + # this is to ensure folder symlinks are supported, + # which are important for ease of development. + for path in glob.iglob(f"{BASE_PATH}/**/*.py", recursive=True): + + log.trace("Path: {0}".format(path)) + + # calculate the module name, dervived from the relative path + relative_path = Path(path).relative_to(BASE_PATH) name = relative_path.__str__().rstrip(".py").replace("/", ".") - name = "modmail.plugins." + name - log.trace("Relative path: {0}".format(name)) + name = PLUGIN_MODULE + "." + name + log.trace("Module name: {0}".format(name)) if unqualify(name.split(".")[-1]).startswith("_"): # Ignore module/package names starting with an underscore. From 22824c345d912ee22305295b426c3aacd55aef62 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Wed, 11 Aug 2021 23:42:10 -0400 Subject: [PATCH 157/169] changelog: document that plugins can be symlinked folders --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a79da443..c8963bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - This is not used yet. - Extension loading system (#43) - scans for extensions in the `modmail/extensions` folder and loads them if they are of the right format. + - all extensions must be loadable as a module, which means they must have `__init__.py` files in their directories. - Plugin loading system (#43) - scans for plugins in the `modmail/plugins` folder and loads them. + - Unlike extensions, plugins and their respective folders do not need to have `__init__.py` files and are allowed to be symlinks. - Extension management commands (#43) - load, reload, unload, list, refresh commands for dealing with extensions - Run the `ext` command for more details when bot is in `DEVELOP` mode. From 388a7734b758391bde8d45f81438555ff8534834 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 12 Aug 2021 00:15:57 -0400 Subject: [PATCH 158/169] fix: rewalking extensions keeps loaded exts --- modmail/extensions/extension_manager.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 940c17b0..b1d52bc3 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -198,9 +198,15 @@ async def rewalk_extensions(self, ctx: Context) -> None: log.debug(f"Refreshing list of {self.type}s.") # make sure the new walk contains all currently loaded extensions, so they can be unloaded - loaded_extensions = self.bot.extensions & self.all_extensions.keys() + loaded_extensions = dict() + for ext in self.all_extensions.items(): + if ext in self.bot.extensions: + loaded_extensions.update(ext) + # now that we know what the list was, we can clear it self.all_extensions.clear() + # put the loaded extensions back in self.all_extensions.update(loaded_extensions) + # now we can re-walk the extensions self.all_extensions.update(self.refresh_method()) await ctx.send(f":ok_hand: Refreshed list of {self.type}s.") From e5f70f0543381d2bf0e66b731cc988b8dea09a32 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 12 Aug 2021 00:43:36 -0400 Subject: [PATCH 159/169] fix: don't break help command in dms --- modmail/extensions/plugin_manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 9fc1f47b..4710ad55 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -94,7 +94,10 @@ async def rewalk_plugins(self, ctx: Context) -> None: # This cannot be static (must have a __func__ attribute). async def cog_check(self, ctx: Context) -> bool: """Only allow server admins and bot owners to invoke the commands in this cog.""" - return ctx.author.guild_permissions.administrator or await self.bot.is_owner(ctx.author) + if ctx.guild is None: + return await self.bot.is_owner(ctx.author) + else: + return ctx.author.guild_permissions.administrator or await self.bot.is_owner(ctx.author) # HACK: Delete the commands from ExtensionManager that PluginManager has inherited From f3571eab0749b33ae4586dc8fd7ad1b0556d931b Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Thu, 12 Aug 2021 19:27:21 +0530 Subject: [PATCH 160/169] fix(exts): Fix all loaded cogs check while resyncing cogs Originally when you would resync the cogs, it would check whether `(ext, status)` is present in the extensions which is a dict. But this would always return `False`, for example ```pycon a = {"a": "a"} >>> print(("a", "a") in a) False ``` A fix to this, as committed is to just check whether `ext` is present in the extension keys. --- modmail/extensions/extension_manager.py | 8 +++----- modmail/extensions/plugin_manager.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index b1d52bc3..003865b9 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -188,7 +188,7 @@ async def list_extensions(self, ctx: Context) -> None: # TODO: since we currently don't have a paginator. await ctx.send("".join(lines) or f"There are no {self.type}s installed.") - @extensions_group.command(name="refresh", aliases=("rewalk",)) + @extensions_group.command(name="refresh", aliases=("rewalk", "rescan")) async def rewalk_extensions(self, ctx: Context) -> None: """ Refreshes the list of extensions. @@ -198,10 +198,8 @@ async def rewalk_extensions(self, ctx: Context) -> None: log.debug(f"Refreshing list of {self.type}s.") # make sure the new walk contains all currently loaded extensions, so they can be unloaded - loaded_extensions = dict() - for ext in self.all_extensions.items(): - if ext in self.bot.extensions: - loaded_extensions.update(ext) + loaded_extensions = dict((en, sl) for en, sl in self.all_extensions.items() if en in self.bot.extensions) + # now that we know what the list was, we can clear it self.all_extensions.clear() # put the loaded extensions back in diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 4710ad55..6637c280 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -84,7 +84,7 @@ async def list_plugins(self, ctx: Context) -> None: """ await self.list_extensions.callback(self, ctx) - @plugins_group.command(name="refresh", aliases=("rewalk",)) + @plugins_group.command(name="refresh", aliases=("rewalk", "rescan")) async def rewalk_plugins(self, ctx: Context) -> None: """Refreshes the list of installed plugins.""" await self.rewalk_extensions.callback(self, ctx) From 0abd87bed94eb5743a43c48b4629de79fc7826c2 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Thu, 12 Aug 2021 19:53:39 +0530 Subject: [PATCH 161/169] chore: Add comments to explain some code logic --- modmail/__init__.py | 2 +- modmail/extensions/extension_manager.py | 7 +++++-- modmail/extensions/plugin_manager.py | 4 ++-- modmail/utils/cogs.py | 1 + 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index c87eca1a..c50bf7e3 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -26,7 +26,7 @@ # file handler file_handler = logging.handlers.RotatingFileHandler( log_file, - maxBytes=5 * (2 ** 14), + maxBytes=5 * (2 ** 14), # 81920 bytes, approximately 200 lines backupCount=7, encoding="utf-8", ) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 003865b9..8361fbc1 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -189,7 +189,7 @@ async def list_extensions(self, ctx: Context) -> None: await ctx.send("".join(lines) or f"There are no {self.type}s installed.") @extensions_group.command(name="refresh", aliases=("rewalk", "rescan")) - async def rewalk_extensions(self, ctx: Context) -> None: + async def resync_extensions(self, ctx: Context) -> None: """ Refreshes the list of extensions. @@ -198,7 +198,9 @@ async def rewalk_extensions(self, ctx: Context) -> None: log.debug(f"Refreshing list of {self.type}s.") # make sure the new walk contains all currently loaded extensions, so they can be unloaded - loaded_extensions = dict((en, sl) for en, sl in self.all_extensions.items() if en in self.bot.extensions) + loaded_extensions = dict( + (en, sl) for en, sl in self.all_extensions.items() if en in self.bot.extensions + ) # now that we know what the list was, we can clear it self.all_extensions.clear() @@ -268,6 +270,7 @@ def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: msg = f":x: {self.type.capitalize()} `{ext}` is already {verb}ed." except Exception as e: if hasattr(e, "original"): + # If original exception is present, then utilize it e = e.original log.exception(f"{self.type.capitalize()} '{ext}' failed to {verb}.") diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 6637c280..e7688529 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -85,9 +85,9 @@ async def list_plugins(self, ctx: Context) -> None: await self.list_extensions.callback(self, ctx) @plugins_group.command(name="refresh", aliases=("rewalk", "rescan")) - async def rewalk_plugins(self, ctx: Context) -> None: + async def resync_plugins(self, ctx: Context) -> None: """Refreshes the list of installed plugins.""" - await self.rewalk_extensions.callback(self, ctx) + await self.resync_extensions.callback(self, ctx) # TODO: Implement install/enable/disable/etc diff --git a/modmail/utils/cogs.py b/modmail/utils/cogs.py index b0003851..5d5a92b8 100644 --- a/modmail/utils/cogs.py +++ b/modmail/utils/cogs.py @@ -8,6 +8,7 @@ class BitwiseAutoEnum(IntEnum): """Enum class which generates binary value for each item.""" def _generate_next_value_(name, start, count, last_values) -> int: # noqa: ANN001 N805 + """Override default enum auto() counter to return increasing powers of 2, 4, 8...""" return 1 << count From 40e99d8973c5f659db5cc4f5cf7675e2e1dc7024 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Thu, 12 Aug 2021 19:57:36 +0530 Subject: [PATCH 162/169] docs(contrib): Mention changes by Bast --- CONTRIBUTING.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 706927b3..2a90f8dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,7 +67,7 @@ Once it's filed: - The Maintainers will evaluate the feature request, possibly asking you more questions to understand its purpose and any relevant requirements. If the issue is closed, the team will convey their reasoning and suggest an alternative path forward. - If the feature request is accepted, it will be marked for implementation with `status: approved`, which can then be done either by a core team member or by anyone in the community who wants to contribute code. -> **Note**: The team is unlikely to be able to accept every single feature request that is filed. Please understand if they need to say no. +> **Note**: The team is unlikely to be able to accept every single feature request that is filed. Please understand if they need to say no. However for most features requested, you can always write a plugin for your modmail bot. ## Project Setup @@ -80,7 +80,11 @@ You will need your own test server and bot account on Discord to test your chang 1. Create a test server. 1. Create a bot account and invite it to the server you just created. -Note down the IDs for your server, as well as any channels and roles created. + + +Note down the IDs for your server. Learn how to obtain the ID of a server, channel or role **[here](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-).** ### Fork the Project @@ -164,7 +168,7 @@ To contribute code: - Run `flake8`, `black` and `pre-commit` against your code **before** you push. Your commit will be rejected by the build server if it fails to lint. You can run the lint by executing `poetry run task lint` in your command line. - Go to [discord-modmail/modmail/pulls][modmail-pulls] and open a new pull request with your changes. - If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This permits maintainers to commit changes directly to your fork, speeding up the review process. -- If your PR is connected to an open issue, add a line in your PR's description that says `Closes #123`, where `#123` is the number of the issue you're fixing. +- If your PR is connected to an open issue, add a line in your PR's description that says `Closes #123`, where `#123` is the number of the issue you're fixing. This will make github link your issue, and make it easier for us (and other contributers) to find and understand the context behind your PR. > Pull requests (or PRs for short) are the primary mechanism we use to change modmail. GitHub itself has some [great documentation][about-pull-requests] on using the Pull Request feature. We use the "fork and pull" model [described here][development-models], where contributors push changes to their personal fork and create pull requests to bring those changes into the source repository. From 5d8e683ca743411c03460971e92cf50834f8e484 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Thu, 12 Aug 2021 20:08:35 +0530 Subject: [PATCH 163/169] chore: Mention why we use iglob over Pathlib's glob --- modmail/utils/plugins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modmail/utils/plugins.py b/modmail/utils/plugins.py index 21c4af57..e6f681ac 100644 --- a/modmail/utils/plugins.py +++ b/modmail/utils/plugins.py @@ -34,6 +34,8 @@ def walk_plugins() -> t.Iterator[t.Tuple[str, bool]]: # walk all files in the plugins folder # this is to ensure folder symlinks are supported, # which are important for ease of development. + # NOTE: We are not using Pathlib's glob utility as it doesn't + # support following symlinks, see: https://bugs.python.org/issue33428 for path in glob.iglob(f"{BASE_PATH}/**/*.py", recursive=True): log.trace("Path: {0}".format(path)) From d078f3191adaa382b56f87113d5d79182c4cbdef Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Fri, 13 Aug 2021 02:23:01 +0530 Subject: [PATCH 164/169] chore: Make resync command docstring more obvious/clear --- modmail/extensions/extension_manager.py | 2 +- modmail/extensions/plugin_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 8361fbc1..60aed877 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -191,7 +191,7 @@ async def list_extensions(self, ctx: Context) -> None: @extensions_group.command(name="refresh", aliases=("rewalk", "rescan")) async def resync_extensions(self, ctx: Context) -> None: """ - Refreshes the list of extensions. + Refreshes the list of extensions from disk, but do not unload any currently active. Typical use case is in the event that the existing extensions have changed while the bot is running. """ diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index e7688529..208c2289 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -86,7 +86,7 @@ async def list_plugins(self, ctx: Context) -> None: @plugins_group.command(name="refresh", aliases=("rewalk", "rescan")) async def resync_plugins(self, ctx: Context) -> None: - """Refreshes the list of installed plugins.""" + """ Refreshes the list of plugins from disk, but do not unload any currently active.""" await self.resync_extensions.callback(self, ctx) # TODO: Implement install/enable/disable/etc From 662872bfd9291c913877522f031f0f4b89fc5a15 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Fri, 13 Aug 2021 02:28:02 +0530 Subject: [PATCH 165/169] chore: Only use `*` (remove `**`) for selecting all extensions --- modmail/extensions/extension_manager.py | 16 ++++++++-------- modmail/extensions/plugin_manager.py | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index 60aed877..d4475a02 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -37,7 +37,7 @@ class ExtensionConverter(commands.Converter): """ Fully qualify the name of an extension and ensure it exists. - The * and ** values bypass this when used with the reload command. + The * value bypasses this when used with the an extension manger command. """ source_list = EXTENSIONS @@ -46,7 +46,7 @@ class ExtensionConverter(commands.Converter): async def convert(self, _: Context, argument: str) -> str: """Fully qualify the name of an extension and ensure it exists.""" # Special values to reload all extensions - if argument == "*" or argument == "**": + if argument == "*": return argument argument = argument.lower() @@ -104,13 +104,13 @@ async def load_extensions(self, ctx: Context, *extensions: ExtensionConverter) - """ Load extensions given their fully qualified or unqualified names. - If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. + If '\*' is given as the name, all unloaded extensions will be loaded. """ # noqa: W605 if not extensions: await ctx.send_help(ctx.command) return - if "*" in extensions or "**" in extensions: + if "*" in extensions: extensions = sorted(ext for ext in self.all_extensions if ext not in self.bot.extensions.keys()) msg = self.batch_manage(Action.LOAD, *extensions) @@ -121,7 +121,7 @@ async def unload_extensions(self, ctx: Context, *extensions: ExtensionConverter) """ Unload currently loaded extensions given their fully qualified or unqualified names. - If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. + If '\*' is given as the name, all loaded extensions will be unloaded. """ # noqa: W605 if not extensions: await ctx.send_help(ctx.command) @@ -134,7 +134,7 @@ async def unload_extensions(self, ctx: Context, *extensions: ExtensionConverter) await ctx.send(f":x: The following {self.type}(s) may not be unloaded:```\n{bl_msg}```") return - if "*" in extensions or "**" in extensions: + if "*" in extensions: extensions = sorted( ext for ext in self.bot.extensions.keys() & self.all_extensions @@ -150,13 +150,13 @@ async def reload_extensions(self, ctx: Context, *extensions: ExtensionConverter) If an extension fails to be reloaded, it will be rolled-back to the prior working state. - If '\*' or '\*\*' is given as the name, all currently loaded extensions will be reloaded. + If '\*' is given as the name, all currently loaded extensions will be reloaded. """ # noqa: W605 if not extensions: await ctx.send_help(ctx.command) return - if "*" in extensions or "**" in extensions: + if "*" in extensions: extensions = self.bot.extensions.keys() & self.all_extensions.keys() await ctx.send(self.batch_manage(Action.RELOAD, *extensions)) diff --git a/modmail/extensions/plugin_manager.py b/modmail/extensions/plugin_manager.py index 208c2289..4661a7ae 100644 --- a/modmail/extensions/plugin_manager.py +++ b/modmail/extensions/plugin_manager.py @@ -13,7 +13,7 @@ class PluginConverter(ExtensionConverter): """ Fully qualify the name of a plugin and ensure it exists. - The \* and \*\* values bypass this when used with the reload command. + The * value bypasses this when used with the a plugin manger command. """ # noqa: W605 source_list = PLUGINS @@ -50,7 +50,7 @@ async def load_plugin(self, ctx: Context, *plugins: PluginConverter) -> None: """ Load plugins given their fully qualified or unqualified names. - If '\*' or '\*\*' is given as the name, all unloaded plugins will be loaded. + If '\*' is given as the name, all unloaded plugins will be loaded. """ # noqa: W605 await self.load_extensions.callback(self, ctx, *plugins) @@ -59,7 +59,7 @@ async def unload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: """ Unload currently loaded plugins given their fully qualified or unqualified names. - If '\*' or '\*\*' is given as the name, all loaded plugins will be unloaded. + If '\*' is given as the name, all loaded plugins will be unloaded. """ # noqa: W605 await self.unload_extensions.callback(self, ctx, *plugins) @@ -70,7 +70,7 @@ async def reload_plugins(self, ctx: Context, *plugins: PluginConverter) -> None: If an plugin fails to be reloaded, it will be rolled-back to the prior working state. - If '\*' or '\*\*' is given as the name, all currently loaded plugins will be reloaded. + If '\*' is given as the name, all currently loaded plugins will be reloaded. """ # noqa: W605 await self.reload_extensions.callback(self, ctx, *plugins) @@ -86,7 +86,7 @@ async def list_plugins(self, ctx: Context) -> None: @plugins_group.command(name="refresh", aliases=("rewalk", "rescan")) async def resync_plugins(self, ctx: Context) -> None: - """ Refreshes the list of plugins from disk, but do not unload any currently active.""" + """Refreshes the list of plugins from disk, but do not unload any currently active.""" await self.resync_extensions.callback(self, ctx) # TODO: Implement install/enable/disable/etc From 24354e779c3fb56c2773bc3da50fc5fbad930baf Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 12 Aug 2021 20:40:55 -0400 Subject: [PATCH 166/169] chore: remove duplicate code --- modmail/utils/extensions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modmail/utils/extensions.py b/modmail/utils/extensions.py index 661f157d..2734bfa0 100644 --- a/modmail/utils/extensions.py +++ b/modmail/utils/extensions.py @@ -21,6 +21,11 @@ NO_UNLOAD: t.List[str] = list() +def unqualify(name: str) -> str: + """Return an unqualified name given a qualified module/package `name`.""" + return name.rsplit(".", maxsplit=1)[-1] + + def determine_bot_mode() -> int: """ Figure out the bot mode from the configuration system. @@ -29,7 +34,7 @@ def determine_bot_mode() -> int: """ bot_mode = 0 for mode in BotModes: - if getattr(CONFIG.dev.mode, str(mode).split(".")[-1].lower(), True): + if getattr(CONFIG.dev.mode, unqualify(str(mode)).lower(), True): bot_mode += mode.value return bot_mode @@ -42,11 +47,6 @@ def determine_bot_mode() -> int: log.debug(f"Plugin dev mode status: {bool(BOT_MODE & BOT_MODES.PLUGIN_DEV)}") -def unqualify(name: str) -> str: - """Return an unqualified name given a qualified module/package `name`.""" - return name.rsplit(".", maxsplit=1)[-1] - - def walk_extensions() -> t.Iterator[t.Tuple[str, t.Tuple[bool, bool]]]: """Yield extension names from the modmail.exts subpackage.""" From eddbf3af94aecc79632dee1ad5f000e3aae06c51 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 12 Aug 2021 20:43:49 -0400 Subject: [PATCH 167/169] docs: prep changelog for release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8963bb2..8dafbd12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.1.0] +## [0.1.0] - 2021-08-13 ### Added From f532582f868a2027765a910a7468d7f291b05918 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 13 Aug 2021 13:22:28 -0400 Subject: [PATCH 168/169] bump log size to 8MB --- modmail/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modmail/__init__.py b/modmail/__init__.py index c50bf7e3..14cd73a5 100644 --- a/modmail/__init__.py +++ b/modmail/__init__.py @@ -11,6 +11,9 @@ logging.addLevelName(logging.TRACE, "TRACE") logging.addLevelName(logging.NOTICE, "NOTICE") + +LOG_FILE_SIZE = 8 * (2 ** 10) ** 2 # 8MB, discord upload limit + # this logging level is set to logging.TRACE because if it is not set to the lowest level, # the child level will be limited to the lowest level this is set to. ROOT_LOG_LEVEL = logging.TRACE @@ -26,7 +29,7 @@ # file handler file_handler = logging.handlers.RotatingFileHandler( log_file, - maxBytes=5 * (2 ** 14), # 81920 bytes, approximately 200 lines + maxBytes=LOG_FILE_SIZE, backupCount=7, encoding="utf-8", ) From f708632430953b8d79e89dda2962d65dc2e63c95 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 13 Aug 2021 13:35:43 -0400 Subject: [PATCH 169/169] chore: don't use list compression --- modmail/extensions/extension_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modmail/extensions/extension_manager.py b/modmail/extensions/extension_manager.py index d4475a02..2f5c5a41 100644 --- a/modmail/extensions/extension_manager.py +++ b/modmail/extensions/extension_manager.py @@ -198,9 +198,10 @@ async def resync_extensions(self, ctx: Context) -> None: log.debug(f"Refreshing list of {self.type}s.") # make sure the new walk contains all currently loaded extensions, so they can be unloaded - loaded_extensions = dict( - (en, sl) for en, sl in self.all_extensions.items() if en in self.bot.extensions - ) + loaded_extensions = {} + for name, should_load in self.all_extensions.items(): + if name in self.bot.extensions: + loaded_extensions[name] = should_load # now that we know what the list was, we can clear it self.all_extensions.clear()