From 85fdadd3a8d427fbcae7252cc29011cf54732123 Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sat, 24 Aug 2019 02:16:06 -0400 Subject: [PATCH 01/48] Update recipient_thread_close --- core/config_help.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/config_help.json b/core/config_help.json index 2d9a9d68e2..c24b5868fa 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -39,7 +39,7 @@ "`{prefix}mention Yo~ Here's a new thread for ya!`" ], "notes": [ - "Unfortunately, its not currently possible to disable mention." + "Unfortunately, its not currently possible to disable mention. You do not have to include a mention." ] }, "main_color": { @@ -163,8 +163,8 @@ "default": "Disabled", "description": "Setting this configuration will allow recipients to use the `close_emoji` to close the thread themselves.", "examples": [ - "`{prefix}config set reply_without_command yes`", - "`{prefix}config set reply_without_command no`" + "`{prefix}config set recipient_thread_close yes`", + "`{prefix}config set recipient_thread_close no`" ], "notes": [ "The close emoji is dictated by the configuration `close_emoji`.", @@ -446,4 +446,4 @@ "This configuration can only to be set through `.env` file or environment (config) variables." ] } -} \ No newline at end of file +} From 9ec5cb79c2d154bf0b827894556435db509655ec Mon Sep 17 00:00:00 2001 From: DAzVise <52792999+DAzVise@users.noreply.github.com> Date: Tue, 17 Sep 2019 14:04:16 +0300 Subject: [PATCH 02/48] Update modmail.py --- cogs/modmail.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index f8246e17e2..a407232182 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -894,12 +894,16 @@ async def contact( await thread.wait_until_ready() embed = discord.Embed( title="Created thread", - description=f"Thread started in {thread.channel.mention} " + description=f"Thread started by {ctx.author.mention} " f"for {user.mention}.", color=self.bot.main_color, ) - await ctx.send(embed=embed) + try: + await thread.channel.send(embed=embed) + except: + await ctx.send(embed=embed) + await ctx.message.delete() @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) From 1340d733d51c7c0f57f8091d1a587d2aa794aa2c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 17 Sep 2019 20:36:55 -0700 Subject: [PATCH 03/48] Added changelog for v3.2.3-pre and slighty changed contact logic --- CHANGELOG.md | 7 +++++++ bot.py | 2 +- cogs/modmail.py | 11 ++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9532ce987..1f6c4c92da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). + +# [UNRELEASED] + +### Changed + +- `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) + # v3.2.2 Security update! diff --git a/bot.py b/bot.py index a25777160d..4d7014c4d2 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.2.2" +__version__ = "3.2.3-pre" import asyncio import logging diff --git a/cogs/modmail.py b/cogs/modmail.py index a407232182..1ccdcc4a5a 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -886,24 +886,21 @@ async def contact( description="A thread for this user already " f"exists in {exists.channel.mention}.", ) + await ctx.channel.send(embed=embed) else: thread = self.bot.threads.create( user, creator=ctx.author, category=category ) - await thread.wait_until_ready() embed = discord.Embed( - title="Created thread", + title="Created Thread", description=f"Thread started by {ctx.author.mention} " f"for {user.mention}.", color=self.bot.main_color, ) - - try: + await thread.wait_until_ready() await thread.channel.send(embed=embed) - except: - await ctx.send(embed=embed) - await ctx.message.delete() + await ctx.message.delete() @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) From c236b609ec59b108139a57cacf24696f207c2e4c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 19 Sep 2019 21:14:35 -0700 Subject: [PATCH 04/48] 3.3.0-dev --- CHANGELOG.md | 13 + Dockerfile | 1 - bot.py | 132 +-- cogs/modmail.py | 79 +- cogs/plugins.py | 688 +++++++------ cogs/utility.py | 78 +- core/_color_data.py | 2276 ++++++++++++++++++++--------------------- core/changelog.py | 27 +- core/config.py | 157 +-- core/config_help.json | 9 + core/thread.py | 35 +- plugins/registry.json | 2 +- 12 files changed, 1753 insertions(+), 1744 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f6c4c92da..738f7a3927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,19 @@ however, insignificant breaking changes does not guarantee a major version bump, ### Changed - `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) +- Plugins update (mostly internal). + - `git` is no longer used to install plugins, it now downloads through zip files. + - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. + - Reorganised plugins folder structure. + - Logging / plugin-related messages changes. + - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). +- Help command is in alphabetical order grouped by permissions. + +### Internal + +- Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting. + - To get/set the raw value, access through `config[]`. +- Prerelease naming scheme is now `x.x.x-dev`. # v3.2.2 diff --git a/Dockerfile b/Dockerfile index 9735f92d08..eadf05e4e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ FROM python:3.7.4-alpine -RUN apk add --no-cache git WORKDIR /modmailbot COPY . /modmailbot RUN pip install --no-cache-dir -r requirements.min.txt diff --git a/bot.py b/bot.py index 4d7014c4d2..9108ddf583 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.2.3-pre" +__version__ = "3.3.0-dev" import asyncio import logging @@ -19,6 +19,7 @@ from aiohttp import ClientSession from emoji import UNICODE_EMOJI from motor.motor_asyncio import AsyncIOMotorClient +from pkg_resources import parse_version from pymongo.errors import ConfigurationError try: @@ -32,7 +33,7 @@ from core import checks from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import human_join, strtobool, parse_alias +from core.utils import human_join, parse_alias from core.models import PermissionLevel, ModmailLogger, SafeFormatter from core.thread import ThreadManager from core.time import human_timedelta @@ -70,6 +71,7 @@ def __init__(self): self._api = None self.metadata_loop = None self.formatter = SafeFormatter() + self.loaded_cogs = ['cogs.modmail', 'cogs.plugins', 'cogs.utility'] self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -97,17 +99,7 @@ def __init__(self): sys.exit(0) self.plugin_db = PluginDatabaseClient(self) - - logger.line() - logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") - logger.info("││││ │ │││││├─┤││") - logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") - logger.info("v%s", __version__) - logger.info("Authors: kyb3r, fourjr, Taaku18") - logger.line() - - self._load_extensions() - logger.line() + self.startup() @property def uptime(self) -> str: @@ -123,6 +115,24 @@ def uptime(self) -> str: return self.formatter.format(fmt, d=days, h=hours, m=minutes, s=seconds) + def startup(self): + logger.line() + logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") + logger.info("││││ │ │││││├─┤││") + logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") + logger.info("v%s", __version__) + logger.info("Authors: kyb3r, fourjr, Taaku18") + logger.line() + + for cog in self.loaded_cogs: + logger.info("Loading %s.", cog) + try: + self.load_extension(cog) + logger.info("Successfully loaded %s.", cog) + except Exception: + logger.exception("Failed to load %s.", cog) + logger.line() + def _configure_logging(self): level_text = self.config["log_level"].upper() logging_levels = { @@ -161,8 +171,8 @@ def _configure_logging(self): logger.debug("Successfully configured logging.") @property - def version(self) -> str: - return __version__ + def version(self): + return parse_version(__version__) @property def session(self) -> ClientSession: @@ -179,18 +189,6 @@ def api(self): async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] - def _load_extensions(self): - """Adds commands automatically""" - for file in os.listdir("cogs"): - if not file.endswith(".py"): - continue - cog = f"cogs.{file[:-3]}" - logger.info("Loading %s.", cog) - try: - self.load_extension(cog) - except Exception: - logger.exception("Failed to load %s.", cog) - def run(self, *args, **kwargs): try: self.loop.run_until_complete(self.start(self.token)) @@ -366,25 +364,17 @@ def blocked_whitelisted_users(self) -> typing.List[str]: def prefix(self) -> str: return str(self.config["prefix"]) - def _parse_color(self, conf_name): - color = self.config[conf_name] - try: - return int(color.lstrip("#"), base=16) - except ValueError: - logger.error("Invalid %s provided.", conf_name) - return int(self.config.remove(conf_name).lstrip("#"), base=16) - @property def mod_color(self) -> int: - return self._parse_color("mod_color") + return self.config.get("mod_color") @property def recipient_color(self) -> int: - return self._parse_color("recipient_color") + return self.config.get("recipient_color") @property def main_color(self) -> int: - return self._parse_color("main_color") + return self.config.get("main_color") def command_perm(self, command_name: str) -> PermissionLevel: level = self.config["override_command_level"].get(command_name) @@ -518,7 +508,6 @@ async def on_ready(self): loop=None, ) self.metadata_loop.before_loop(self.before_post_metadata) - self.metadata_loop.after_loop(self.after_post_metadata) self.metadata_loop.start() async def convert_emoji(self, name: str) -> str: @@ -574,38 +563,14 @@ async def _process_blocked(self, message: discord.Message) -> bool: now = datetime.utcnow() - account_age = self.config["account_age"] - guild_age = self.config["guild_age"] + account_age = self.config.get("account_age") + guild_age = self.config.get("guild_age") if account_age is None: account_age = isodate.Duration() if guild_age is None: guild_age = isodate.Duration() - if not isinstance(account_age, isodate.Duration): - try: - account_age = isodate.parse_duration(account_age) - except isodate.ISO8601Error: - logger.warning( - "The account age limit needs to be a " - "ISO-8601 duration formatted duration string " - 'greater than 0 days, not "%s".', - str(account_age), - ) - account_age = self.config.remove("account_age") - - if not isinstance(guild_age, isodate.Duration): - try: - guild_age = isodate.parse_duration(guild_age) - except isodate.ISO8601Error: - logger.warning( - "The guild join age limit needs to be a " - "ISO-8601 duration formatted duration string " - 'greater than 0 days, not "%s".', - str(guild_age), - ) - guild_age = self.config.remove("guild_age") - reason = self.blocked_users.get(str(message.author.id)) or "" min_guild_age = min_account_age = now @@ -860,14 +825,7 @@ async def process_commands(self, message): thread = await self.threads.find(channel=ctx.channel) if thread is not None: - try: - reply_without_command = strtobool( - self.config["reply_without_command"] - ) - except ValueError: - reply_without_command = self.config.remove("reply_without_command") - - if reply_without_command: + if self.config.get('reply_without_command'): await thread.reply(message) else: await self.api.append_log(message, type_="internal") @@ -887,11 +845,7 @@ async def _void(*_args, **_kwargs): pass if isinstance(channel, discord.DMChannel): - try: - user_typing = strtobool(self.config["user_typing"]) - except ValueError: - user_typing = self.config.remove("user_typing") - if not user_typing: + if not self.config.get("user_typing"): return thread = await self.threads.find(recipient=user) @@ -899,11 +853,7 @@ async def _void(*_args, **_kwargs): if thread: await thread.channel.trigger_typing() else: - try: - mod_typing = strtobool(self.config["mod_typing"]) - except ValueError: - mod_typing = self.config.remove("mod_typing") - if not mod_typing: + if not self.config.get('mod_typing'): return thread = await self.threads.find(channel=channel) @@ -941,15 +891,7 @@ async def on_raw_reaction_add(self, payload): if isinstance(channel, discord.DMChannel): if str(reaction) == str(close_emoji): # closing thread - try: - recipient_thread_close = strtobool( - self.config["recipient_thread_close"] - ) - except ValueError: - recipient_thread_close = self.config.remove( - "recipient_thread_close" - ) - if not recipient_thread_close: + if not self.config.get('recipient_thread_close'): return thread = await self.threads.find(recipient=user) ts = message.embeds[0].timestamp if message.embeds else None @@ -1146,7 +1088,7 @@ async def post_metadata(self): "member_count": len(self.guild.members), "uptime": (datetime.utcnow() - self.start_time).total_seconds(), "latency": f"{self.ws.latency * 1000:.4f}", - "version": self.version, + "version": str(self.version), "selfhosted": True, "last_updated": str(datetime.utcnow()), } @@ -1161,10 +1103,6 @@ async def before_post_metadata(self): if not self.guild: self.metadata_loop.cancel() - @staticmethod - async def after_post_metadata(): - logger.info("Metadata loop has been cancelled.") - if __name__ == "__main__": try: diff --git a/cogs/modmail.py b/cogs/modmail.py index 1ccdcc4a5a..f51c131b67 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -17,7 +17,7 @@ from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed, format_description, strtobool +from core.utils import format_preview, User, create_not_found_embed, format_description logger = logging.getLogger("Modmail") @@ -302,12 +302,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = await thread.channel.edit(category=category, sync_permissions=True) - try: - thread_move_notify = strtobool(self.bot.config["thread_move_notify"]) - except ValueError: - thread_move_notify = self.bot.config.remove("thread_move_notify") - - if thread_move_notify and not silent: + if self.bot.config('thread_move_notify') and not silent: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], @@ -398,6 +393,17 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): closer=ctx.author, after=close_after, message=message, silent=silent ) + @staticmethod + def parse_user_or_role(ctx, user_or_role): + mention = None + if user_or_role is None: + mention = ctx.author.mention + elif hasattr(user_or_role, "mention"): + mention = user_or_role.mention + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") + return mention + @commands.command(aliases=["alert"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @@ -413,17 +419,12 @@ async def notify( `@here` and `@everyone` can be substituted with `here` and `everyone`. `user_or_role` may be a user ID, mention, name. role ID, mention, name, "everyone", or "here". """ - thread = ctx.thread - - if user_or_role is None: - mention = ctx.author.mention - elif hasattr(user_or_role, "mention"): - mention = user_or_role.mention - elif user_or_role in {"here", "everyone", "@here", "@everyone"}: - mention = "@" + user_or_role.lstrip("@") - else: + mention = self.parse_user_or_role(ctx, user_or_role) + if mention is None: raise commands.BadArgument(f"{user_or_role} is not a valid role.") + thread = ctx.thread + if str(thread.id) not in self.bot.config["notification_squad"]: self.bot.config["notification_squad"][str(thread.id)] = [] @@ -457,17 +458,12 @@ async def unnotify( `@here` and `@everyone` can be substituted with `here` and `everyone`. `user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here". """ - thread = ctx.thread - - if user_or_role is None: - mention = ctx.author.mention - elif hasattr(user_or_role, "mention"): - mention = user_or_role.mention - elif user_or_role in {"here", "everyone", "@here", "@everyone"}: - mention = "@" + user_or_role.lstrip("@") - else: + mention = self.parse_user_or_role(ctx, user_or_role) + if mention is None: mention = f"`{user_or_role}`" + thread = ctx.thread + if str(thread.id) not in self.bot.config["notification_squad"]: self.bot.config["notification_squad"][str(thread.id)] = [] @@ -502,17 +498,12 @@ async def subscribe( `@here` and `@everyone` can be substituted with `here` and `everyone`. `user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here". """ - thread = ctx.thread - - if user_or_role is None: - mention = ctx.author.mention - elif hasattr(user_or_role, "mention"): - mention = user_or_role.mention - elif user_or_role in {"here", "everyone", "@here", "@everyone"}: - mention = "@" + user_or_role.lstrip("@") - else: + mention = self.parse_user_or_role(ctx, user_or_role) + if mention is None: raise commands.BadArgument(f"{user_or_role} is not a valid role.") + thread = ctx.thread + if str(thread.id) not in self.bot.config["subscriptions"]: self.bot.config["subscriptions"][str(thread.id)] = [] @@ -546,17 +537,12 @@ async def unsubscribe( `@here` and `@everyone` can be substituted with `here` and `everyone`. `user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here". """ - thread = ctx.thread - - if user_or_role is None: - mention = ctx.author.mention - elif hasattr(user_or_role, "mention"): - mention = user_or_role.mention - elif user_or_role in {"here", "everyone", "@here", "@everyone"}: - mention = "@" + user_or_role.lstrip("@") - else: + mention = self.parse_user_or_role(ctx, user_or_role) + if mention is None: mention = f"`{user_or_role}`" + thread = ctx.thread + if str(thread.id) not in self.bot.config["subscriptions"]: self.bot.config["subscriptions"][str(thread.id)] = [] @@ -854,7 +840,6 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) - @trigger_typing async def contact( self, ctx, @@ -900,6 +885,12 @@ async def contact( ) await thread.wait_until_ready() await thread.channel.send(embed=embed) + sent_emoji, _ = await self.bot.retrieve_emoji() + try: + await ctx.message.add_reaction(sent_emoji) + except (discord.HTTPException, discord.InvalidArgument): + pass + await asyncio.sleep(3) await ctx.message.delete() @commands.group(invoke_without_command=True) diff --git a/cogs/plugins.py b/cogs/plugins.py index 4ea76503be..e15b47e29b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -1,15 +1,19 @@ import asyncio -import importlib +import io import json import logging import os import shutil -import site -import stat -import subprocess import sys import typing +import zipfile + +from importlib import invalidate_caches from difflib import get_close_matches +from pathlib import Path, PurePath +from re import match +from site import USER_SITE +from subprocess import PIPE import discord from discord.ext import commands @@ -18,14 +22,68 @@ from core import checks from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession +from core.utils import truncate logger = logging.getLogger("Modmail") -class DownloadError(Exception): +class InvalidPluginError(commands.BadArgument): pass +class Plugin: + def __init__(self, user, repo, name, branch=None): + self.user = user + self.repo = repo + self.name = name + self.branch = branch if branch is not None else 'master' + self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip" + self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}" + + @property + def path(self): + return PurePath('plugins') / self.user / self.repo / f'{self.name}-{self.branch}' + + @property + def abs_path(self): + return Path(__file__).absolute().parent.parent / self.path + + @property + def cache_path(self): + return Path(__file__).absolute().parent.parent / 'temp' / \ + 'plugins-cache' / f'{self.user}-{self.repo}-{self.branch}.zip' + + @property + def ext_string(self): + return f'plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}' + + def __str__(self): + return f'{self.user}/{self.repo}/{self.name}@{self.branch}' + + def __lt__(self, other): + return self.name.lower() < other.name.lower() + + @classmethod + def from_string(cls, s, strict=False): + if not strict: + m = match(r'^(.+?)/(.+?)/(.+?)(?:@(.+?))?$', s) + else: + m = match(r'^(.+?)/(.+?)/(.+?)@(.+?)$', s) + if m is not None: + return Plugin(*m.groups()) + else: + raise InvalidPluginError('Cannot decipher %s.', s) + + def __hash__(self): + return hash((self.user, self.repo, self.name, self.branch)) + + def __repr__(self): + return f'' + + def __eq__(self, other): + return isinstance(other, Plugin) and self.__str__() == other.__str__() + + class Plugins(commands.Cog): """ Plugins expand Modmail functionality by allowing third-party addons. @@ -39,7 +97,10 @@ class Plugins(commands.Cog): def __init__(self, bot): self.bot = bot self.registry = {} - self.bot.loop.create_task(self.download_initial_plugins()) + self.loaded_plugins = set() + self._ready_event = asyncio.Event() + + self.bot.loop.create_task(self.initial_load_plugins()) self.bot.loop.create_task(self.populate_registry()) async def populate_registry(self): @@ -47,357 +108,344 @@ async def populate_registry(self): async with self.bot.session.get(url) as resp: self.registry = json.loads(await resp.text()) - @staticmethod - def _asubprocess_run(cmd): - return subprocess.run(cmd, shell=True, check=True, capture_output=True) + async def initial_load_plugins(self): + await self.bot.wait_for_connected() - @staticmethod - def parse_plugin(name): - # returns: (username, repo, plugin_name, branch) - # default branch = master - try: - # when names are formatted with inline code - result = name.split("/") - result[2] = "/".join(result[2:]) - if "@" in result[2]: - # branch is specified - # for example, fourjr/modmail-plugins/welcomer@develop is a valid name - branch_split_result = result[2].split("@") - result.append(branch_split_result[-1]) - result[2] = "@".join(branch_split_result[:-1]) - else: - result.append("master") + for plugin_name in list(self.bot.config["plugins"]): + try: + plugin = Plugin.from_string(plugin_name, strict=True) + except InvalidPluginError: + self.bot.config["plugins"].remove(plugin_name) + try: + # For backwards compat + plugin = Plugin.from_string(plugin_name) + except InvalidPluginError: + logger.error("Failed to parse plugin name: %s.", plugin_name, exc_info=True) + continue - except IndexError: - return None + logger.info("Migrated legacy plugin name: %s, now %s.", plugin_name, str(plugin)) + self.bot.config["plugins"].append(str(plugin)) - return tuple(result) + try: + await self.download_plugin(plugin) + await self.load_plugin(plugin) + except Exception: + logger.error("Error when loading plugin %s.", plugin, exc_info=True) + continue - async def download_initial_plugins(self): - await self.bot.wait_for_connected() + logger.info("Finished loading all plugins.") + self._ready_event.set() + await self.bot.config.update() - for i in self.bot.config["plugins"]: - username, repo, name, branch = self.parse_plugin(i) + async def download_plugin(self, plugin, force=False): + if plugin.abs_path.exists() and not force: + return - try: - await self.download_plugin_repo(username, repo, branch) - except DownloadError as exc: - msg = f"{username}/{repo}@{branch} - {exc}" - logger.error(msg, exc_info=True) - else: - try: - await self.load_plugin(username, repo, name, branch) - except DownloadError as exc: - msg = f"{username}/{repo}@{branch}[{name}] - {exc}" - logger.error(msg, exc_info=True) + plugin.abs_path.mkdir(parents=True, exist_ok=True) - async def download_plugin_repo(self, username, repo, branch): - try: - cmd = f"git clone https://github.com/{username}/{repo} " - cmd += f"plugins/{username}-{repo}-{branch} " - cmd += f"-b {branch} -q" - - await self.bot.loop.run_in_executor(None, self._asubprocess_run, cmd) - # -q (quiet) so there's no terminal output unless there's an error - except subprocess.CalledProcessError as exc: - err = exc.stderr.decode("utf-8").strip() - - if not err.endswith("already exists and is not an empty directory."): - # don't raise error if the plugin folder exists - msg = f"Download Error: {username}/{repo}@{branch}" - logger.error(msg) - raise DownloadError(err) from exc - - async def load_plugin(self, username, repo, plugin_name, branch): - ext = f"plugins.{username}-{repo}-{branch}.{plugin_name}.{plugin_name}" - dirname = f"plugins/{username}-{repo}-{branch}/{plugin_name}" - - if "requirements.txt" in os.listdir(dirname): + if plugin.cache_path.exists() and not force: + plugin_io = plugin.cache_path.open('rb') + logger.debug('Loading cached %s.', plugin.cache_path) + + else: + async with self.bot.session.get(plugin.url) as resp: + logger.debug('Downloading %s.', plugin.url) + raw = await resp.read() + plugin_io = io.BytesIO(raw) + if not plugin.cache_path.parent.exists(): + plugin.cache_path.parent.mkdir(parents=True) + + with plugin.cache_path.open('wb') as f: + f.write(raw) + + with zipfile.ZipFile(plugin_io) as zipf: + for member in zipf.namelist(): + path = PurePath(member) + if len(path.parts) >= 3 and path.parts[1] == plugin.name: + with zipf.open(member) as src, (plugin.abs_path / Path(*path.parts[2:])).open("wb") as dst: + shutil.copyfileobj(src, dst) + + plugin_io.close() + + async def load_plugin(self, plugin): + if not (plugin.abs_path / f'{plugin.name}.py').exists(): + raise InvalidPluginError(f'{plugin.name}.py not found.') + + req_txt = plugin.abs_path / 'requirements.txt' + + if req_txt.exists(): # Install PIP requirements venv = hasattr(sys, "real_prefix") # in a virtual env - user_install = "--user" if not venv else "" + user_install = ' --user' if not venv else '' + proc = await asyncio.create_subprocess_shell( + f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", + stderr=PIPE, + stdout=PIPE + ) - try: - if os.name == "nt": # Windows - await self.bot.loop.run_in_executor( - None, - self._asubprocess_run, - f"pip install -r {dirname}/requirements.txt {user_install} -q -q", - ) - else: - await self.bot.loop.run_in_executor( - None, - self._asubprocess_run, - f"python3 -m pip install -U -r {dirname}/" - f"requirements.txt {user_install} -q -q", - ) - # -q -q (quiet) - # so there's no terminal output unless there's an error - except subprocess.CalledProcessError as exc: - err = exc.stderr.decode("utf8").strip() - logger.error("Error.", exc_info=True) - if err: - msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]" - logger.error(msg) - raise DownloadError( - f"Unable to download requirements: ```\n{err}\n```" - ) from exc - else: - if not os.path.exists(site.USER_SITE): - os.makedirs(site.USER_SITE) + logger.debug('Downloading requirements for %s.', plugin.ext_string) + + stdout, stderr = await proc.communicate() - sys.path.insert(0, site.USER_SITE) + if stdout: + logger.debug('[stdout]\n%s.', stdout.decode()) + + if stderr: + logger.debug('[stderr]\n%s.', stderr.decode()) + logger.error("Failed to download requirements for %s.", plugin.ext_string, exc_info=True) + raise InvalidPluginError( + f"Unable to download requirements: ```\n{stderr.decode()}\n```" + ) + + if os.path.exists(USER_SITE): + sys.path.insert(0, USER_SITE) - await asyncio.sleep(0.5) try: - self.bot.load_extension(ext) + self.bot.load_extension(plugin.ext_string) + logger.info('Loaded plugin: %s', plugin.ext_string) + self.loaded_plugins.add(plugin) + except commands.ExtensionError as exc: - msg = f"Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]" - logger.error(msg, exc_info=True) - raise DownloadError("Invalid plugin") from exc + logger.error('Plugin load failure: %s', plugin.ext_string, exc_info=True) + raise InvalidPluginError("Cannot load extension, plugin invalid.") from exc + + async def parse_user_input(self, ctx, plugin_name, check_version=False): + + if plugin_name in self.registry: + details = self.registry[plugin_name] + user, repo = details["repository"].split('/', maxsplit=1) + branch = details.get("branch") + + if check_version: + required_version = details.get("bot_version", False) + + if required_version and self.bot.version < parse_version(required_version): + embed = discord.Embed( + description="Your bot's version is too low. " + f"This plugin requires version `{required_version}`.", + color=discord.Color.red(), + ) + await ctx.send(embed=embed) + return + + plugin = Plugin(user, repo, plugin_name, branch) + else: - msg = f"Loaded Plugin: {username}/{repo}@{branch}[{plugin_name}]" - logger.info(msg) + try: + plugin = Plugin.from_string(plugin_name) + except InvalidPluginError: + embed = discord.Embed( + description="Invalid plugin name, double check the plugin name " + "or use one of the following formats: " + "username/repo/plugin, username/repo/plugin@branch.", + color=discord.Color.red(), + ) + await ctx.send(embed=embed) + return + return plugin - @commands.group(aliases=["plugins"], invoke_without_command=True) + @commands.group(aliases=["plugin"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin(self, ctx): - """Plugin handler. Controls the plugins in the bot.""" + async def plugins(self, ctx): + """ + Manage plugins for Modmail. + """ await ctx.send_help(ctx.command) - @plugin.command(name="add", aliases=["install"]) + @plugins.command(name="add", aliases=["install", "load"]) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_add(self, ctx, *, plugin_name: str): - """Add a plugin.""" - - if plugin_name in self.registry: - details = self.registry[plugin_name] - plugin_name = ( - details["repository"] + "/" + plugin_name + "@" + details["branch"] - ) + async def plugins_add(self, ctx, *, plugin_name: str): + """ + Install a new plugin for the bot. - required_version = details["bot_version"] + `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + """ - if parse_version(self.bot.version) < parse_version(required_version): - embed = discord.Embed( - description=f"Your bot's version is too low. This plugin requires version `{required_version}`.", - color=self.bot.main_color, - ) - return await ctx.send(embed=embed) + plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) + if plugin is None: + return - if plugin_name in self.bot.config["plugins"]: + if str(plugin) in self.bot.config["plugins"]: embed = discord.Embed( description="This plugin is already installed.", - color=self.bot.main_color, + color=discord.Color.red(), ) return await ctx.send(embed=embed) - if plugin_name in self.bot.cogs.keys(): + if plugin.name in self.bot.cogs: # another class with the same name embed = discord.Embed( - description="There's another cog installed with the same name.", - color=self.bot.main_color, + description="Cannot install this plugin (dupe cog name).", + color=discord.Color.red(), ) return await ctx.send(embed=embed) embed = discord.Embed( - description="Downloading this plugin...", color=self.bot.main_color + description=f"Starting to download plugin from {plugin.link}...", + color=self.bot.main_color ) - await ctx.send(embed=embed) + msg = await ctx.send(embed=embed) async with ctx.typing(): - if len(plugin_name.split("/")) >= 3: - username, repo, name, branch = self.parse_plugin(plugin_name) - - try: - await self.download_plugin_repo(username, repo, branch) - except Exception as exc: - if not isinstance(exc, DownloadError): - logger.error( - "Unknown error when adding a plugin:", exc_info=True - ) - embed = discord.Embed( - description=f"Unable to fetch this plugin from Github: `{exc}`.", - color=self.bot.main_color, - ) - return await ctx.send(embed=embed) + try: + await self.download_plugin(plugin, force=True) + except Exception: + logger.warning(f"Unable to download plugin %s.", plugin, exc_info=True) - importlib.invalidate_caches() + embed = discord.Embed( + description=f"Failed to download plugin, check logs for error.", + color=discord.Color.red() + ) - try: - await self.load_plugin(username, repo, name, branch) - except Exception as exc: - if not isinstance(exc, DownloadError): - logger.error( - "Unknown error when adding a plugin:", exc_info=True - ) - embed = discord.Embed( - description=f"Unable to load this plugin: `{exc}`.", - color=self.bot.main_color, - ) - return await ctx.send(embed=embed) + return await msg.edit(embed=embed) - # if it makes it here, it has passed all checks and should - # be entered into the config + invalidate_caches() - self.bot.config["plugins"].append(plugin_name) - await self.bot.config.update() + try: + await self.load_plugin(plugin) + except Exception: + logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description="The plugin is installed.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*", - color=self.bot.main_color, - ) - await ctx.send(embed=embed) - else: - embed = discord.Embed( - description="Invalid plugin name format: use plugin-name or " - "username/repo/plugin or username/repo/plugin@branch.", - color=self.bot.main_color, + description=f"Failed to download plugin, check logs for error.", + color=discord.Color.red() ) - await ctx.send(embed=embed) - @plugin.command(name="remove", aliases=["del", "delete"]) + return await msg.edit(embed=embed) + + self.bot.config["plugins"].append(str(plugin)) + await self.bot.config.update() + + embed = discord.Embed( + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*", + color=self.bot.main_color, + ) + return await msg.edit(embed=embed) + + @plugins.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_remove(self, ctx, *, plugin_name: str): - """Remove a plugin.""" + async def plugins_remove(self, ctx, *, plugin_name: str): + """ + Remove an installed plugin of the bot. - if plugin_name in self.registry: - details = self.registry[plugin_name] - plugin_name = ( - details["repository"] + "/" + plugin_name + "@" + details["branch"] + `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + """ + plugin = await self.parse_user_input(ctx, plugin_name) + if plugin is None: + return + + if str(plugin) not in self.bot.config["plugins"]: + embed = discord.Embed( + description="Plugin is not installed.", + color=discord.Color.red(), ) + return await ctx.send(embed=embed) - if plugin_name in self.bot.config["plugins"]: - username, repo, name, branch = self.parse_plugin(plugin_name) - try: - self.bot.unload_extension( - f"plugins.{username}-{repo}-{branch}.{name}.{name}" - ) - except commands.ExtensionNotLoaded: - logger.error("Plugin was never loaded.") + try: + self.bot.unload_extension(plugin.ext_string) + self.loaded_plugins.remove(plugin) + except (commands.ExtensionNotLoaded, KeyError): + logger.warning("Plugin was never loaded.") - self.bot.config["plugins"].remove(plugin_name) + self.bot.config["plugins"].remove(str(plugin)) + await self.bot.config.update() - try: - if not any( - i.startswith(f"{username}/{repo}") - for i in self.bot.config["plugins"] - ): - # if there are no more of such repos, delete the folder - def onerror(func, path, _): - if not os.access(path, os.W_OK): - # Is the error an access error? - os.chmod(path, stat.S_IWUSR) - func(path) - - shutil.rmtree( - f"plugins/{username}-{repo}-{branch}", onerror=onerror - ) - except Exception: - logger.error("Failed to remove plugin %s.", plugin_name, exc_info=True) - self.bot.config["plugins"].append(plugin_name) - return + embed = discord.Embed( + description="The plugin is uninstalled and all its data is erased.", + color=self.bot.main_color, + ) + await ctx.send(embed=embed) - await self.bot.config.update() + @plugins.command(name="update") + @checks.has_permissions(PermissionLevel.OWNER) + async def plugins_update(self, ctx, *, plugin_name: str): + """ + Update a plugin for the bot. + + `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + """ + + plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) + if plugin is None: + return + if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description="The plugin is uninstalled and all its data is erased.", - color=self.bot.main_color, + description="Plugin is not installed.", + color=discord.Color.red(), ) - await ctx.send(embed=embed) - else: + return await ctx.send(embed=embed) + + async with ctx.typing(): + await self.download_plugin(plugin, force=True) + try: + self.bot.unload_extension(plugin.ext_string) + except commands.ExtensionError: + logger.warning("Plugin unload fail.", exc_info=True) + await self.load_plugin(plugin) + embed = discord.Embed( - description="That plugin is not installed.", color=self.bot.main_color + description=f"Successfully updated {plugin.name}.", + color=self.bot.main_color, ) - await ctx.send(embed=embed) + return await ctx.send(embed=embed) - @plugin.command(name="update") + @plugins.command(name="loaded", aliases=["enabled", "installed"]) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_update(self, ctx, *, plugin_name: str): - """Update a plugin.""" + async def plugins_loaded(self, ctx): + """ + Show a list of currently loaded plugins. + """ - if plugin_name in self.registry: - details = self.registry[plugin_name] - plugin_name = ( - details["repository"] + "/" + plugin_name + "@" + details["branch"] + if not self._ready_event.is_set(): + embed = discord.Embed( + description="Plugins are still loading, please try again later.", + color=self.bot.main_color ) + return await ctx.send(embed=embed) - if plugin_name not in self.bot.config["plugins"]: + if not self.loaded_plugins: embed = discord.Embed( - description="That plugin is not installed.", color=self.bot.main_color + description="There are no plugins currently loaded.", + color=discord.Color.red() ) return await ctx.send(embed=embed) - async with ctx.typing(): - username, repo, name, branch = self.parse_plugin(plugin_name) - - try: - cmd = ( - f"cd plugins/{username}-{repo}-{branch} && " - f"git reset --hard origin/{branch} && git fetch --all && git pull" - ) - cmd = await self.bot.loop.run_in_executor( - None, self._asubprocess_run, cmd - ) - except subprocess.CalledProcessError as exc: - err = exc.stderr.decode("utf8").strip() - - embed = discord.Embed( - description=f"An error occurred while updating: {err}.", - color=self.bot.main_color, - ) - logger.error("An error occurred while updating plugin:", exc_info=True) - await ctx.send(embed=embed) - + loaded_plugins = map(str, sorted(self.loaded_plugins)) + pages = ['```\n'] + for plugin in loaded_plugins: + msg = str(plugin) + '\n' + if len(msg) + len(pages[-1]) + 3 <= 2048: + pages[-1] += msg else: - output = cmd.stdout.decode("utf8").strip() + pages[-1] += '```' + pages.append(f'```\n{msg}') - embed = discord.Embed( - description=f"```\n{output}\n```", color=self.bot.main_color - ) - await ctx.send(embed=embed) - - if output != "Already up to date.": - # repo was updated locally, now perform the cog reload - ext = f"plugins.{username}-{repo}-{branch}.{name}.{name}" - self.bot.unload_extension(ext) - - try: - await self.load_plugin(username, repo, name, branch) - except DownloadError as exc: - embed = discord.Embed( - description=f"Unable to start the plugin: `{exc}`.", - color=self.bot.main_color, - ) - logger.error( - "An error occurred while updating plugin:", exc_info=True - ) - await ctx.send(embed=embed) - - @plugin.command(name="enabled", aliases=["installed"]) - @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_enabled(self, ctx): - """Shows a list of currently enabled plugins.""" + if pages[-1][-3:] != '```': + pages[-1] += '```' - if self.bot.config["plugins"]: - msg = "```\n" + "\n".join(sorted(self.bot.config["plugins"])) + "\n```" - embed = discord.Embed(description=msg, color=self.bot.main_color) - await ctx.send(embed=embed) - else: + embeds = [] + for page in pages: embed = discord.Embed( - description="There are no plugins installed.", color=self.bot.main_color + title="Loaded plugins:", + description=page, + color=self.bot.main_color ) - await ctx.send(embed=embed) + embeds.append(embed) + paginator = EmbedPaginatorSession(ctx, *embeds) + await paginator.run() - @plugin.group( + @plugins.group( invoke_without_command=True, name="registry", aliases=["list", "info"] ) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None): + async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None): """ Shows a list of all approved plugins. @@ -440,57 +488,95 @@ async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = No return await ctx.send(embed=embed) - for name, details in registry: - repo = f"https://github.com/{details['repository']}" - url = f"{repo}/tree/master/{name}" + for plugin_name, details in registry: + details = self.registry[plugin_name] + user, repo = details["repository"].split('/', maxsplit=1) + branch = details.get("branch") + + plugin = Plugin(user, repo, plugin_name, branch) embed = discord.Embed( color=self.bot.main_color, description=details["description"], - url=repo, + url=plugin.link, title=details["repository"], ) embed.add_field( - name="Installation", value=f"```{self.bot.prefix}plugins add {name}```" + name="Installation", value=f"```{self.bot.prefix}plugins add {plugin_name}```" ) embed.set_author( - name=details["title"], icon_url=details.get("icon_url"), url=url + name=details["title"], + icon_url=details.get("icon_url"), + url=plugin.link ) + if details.get("thumbnail_url"): embed.set_thumbnail(url=details.get("thumbnail_url")) + if details.get("image_url"): embed.set_image(url=details.get("image_url")) + if plugin in self.loaded_plugins: + embed.set_footer(text="This plugin is currently loaded.") + else: + required_version = details.get("bot_version", False) + if required_version and self.bot.version < parse_version(required_version): + embed.set_footer( + text=f"Your bot is unable to install this plugin, " + f"minimum required version is v{required_version}.") + else: + embed.set_footer(text="Your bot is able to install this plugin.") + embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) paginator.current = index await paginator.run() - @plugin_registry.command(name="compact") + @plugins_registry.command(name="compact", aliases=["slim"]) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_registry_compact(self, ctx): - """Shows a compact view of all plugins within the registry.""" + async def plugins_registry_compact(self, ctx): + """ + Shows a compact view of all plugins within the registry. + """ await self.populate_registry() registry = sorted(self.registry.items(), key=lambda elem: elem[0]) - pages = [""] - - for name, details in registry: - repo = f"https://github.com/{details['repository']}" - url = f"{repo}/tree/{details['branch']}/{name}" - desc = details["description"].replace("\n", "") - fmt = f"[`{name}`]({url}) - {desc}" - length = len(fmt) - len(url) - 4 - fmt = fmt[: 75 + len(url)].strip() + "..." if length > 75 else fmt - if len(fmt) + len(pages[-1]) >= 2048: - pages.append(fmt + "\n") + pages = [''] + + for plugin_name, details in registry: + details = self.registry[plugin_name] + user, repo = details["repository"].split('/', maxsplit=1) + branch = details.get("branch") + + plugin = Plugin(user, repo, plugin_name, branch) + + desc = discord.utils.escape_markdown(details["description"].replace("\n", "")) + + name = f"[`{plugin.name}`]({plugin.link})" + fmt = f"{name} - {desc}" + + if plugin_name in self.loaded_plugins: + limit = 75 - len(plugin_name) - 4 - 8 + len(name) + if limit < 0: + fmt = plugin.name + limit = 75 + fmt = truncate(fmt, limit) + '[loaded]\n' + else: + limit = 75 - len(plugin_name) - 4 + len(name) + if limit < 0: + fmt = plugin.name + limit = 75 + fmt = truncate(fmt, limit) + '\n' + + if len(fmt) + len(pages[-1]) <= 2048: + pages[-1] += fmt else: - pages[-1] += fmt + "\n" + pages.append(fmt) embeds = [] diff --git a/cogs/utility.py b/cogs/utility.py index 8f0986f06b..d54ad362b9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -41,7 +41,7 @@ async def format_cog_help(self, cog, *, no_cog=False): for cmd in await self.filter_commands( cog.get_commands() if not no_cog else cog, sort=True, - key=lambda c: bot.command_perm(c.qualified_name), + key=lambda c: (bot.command_perm(c.qualified_name), c.qualified_name), ): perm_level = bot.command_perm(cmd.qualified_name) if perm_level is PermissionLevel.INVALID: @@ -119,41 +119,34 @@ async def send_cog_help(self, cog): ) return await session.run() - async def send_command_help(self, command): - if not await self.filter_commands([command]): + async def _get_help_embed(self, topic): + if not await self.filter_commands([topic]): return - perm_level = self.context.bot.command_perm(command.qualified_name) + perm_level = self.context.bot.command_perm(topic.qualified_name) if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: perm_level = "NONE" embed = Embed( - title=f"`{self.get_command_signature(command)}`", + title=f"`{self.get_command_signature(topic)}`", color=self.context.bot.main_color, - description=self.process_help_msg(command.help), + description=self.process_help_msg(topic.help), ) - embed.set_footer(text=f"Permission level: {perm_level}") - await self.get_destination().send(embed=embed) + return embed, perm_level + + async def send_command_help(self, command): + topic = await self._get_help_embed(command) + if topic is not None: + topic[0].set_footer(text=f"Permission level: {topic[1]}") + await self.get_destination().send(embed=topic[0]) async def send_group_help(self, group): - if not await self.filter_commands([group]): + topic = await self._get_help_embed(group) + if topic is None: return - - perm_level = self.context.bot.command_perm(group.qualified_name) - if perm_level is not PermissionLevel.INVALID: - perm_level = f"{perm_level.name} [{perm_level}]" - else: - perm_level = "NONE" - - embed = Embed( - title=f"`{self.get_command_signature(group)}`", - color=self.context.bot.main_color, - description=self.process_help_msg(group.help), - ) - - if perm_level: - embed.add_field(name="Permission Level", value=perm_level, inline=False) + embed = topic[0] + embed.add_field(name="Permission Level", value=topic[1], inline=False) format_ = "" length = len(group.commands) @@ -284,7 +277,7 @@ async def changelog(self, ctx, version: str.lower = ""): pass logger.warning("Failed to display changelog.", exc_info=True) await ctx.send( - f"View the changelog here: {changelog.CHANGELOG_URL}#v{version[::2]}" + f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" ) @commands.command(aliases=["bot", "info"]) @@ -308,27 +301,27 @@ async def about(self, ctx): embed.add_field(name="Uptime", value=self.bot.uptime) embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") embed.add_field(name="Version", value=f"`{self.bot.version}`") - embed.add_field(name="Author", value="[`kyb3r`](https://github.com/kyb3r)") + embed.add_field(name="Author", value="[`kyb3r`, `Taki`, `4jr`](https://github.com/kyb3r)") changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version - if parse_version(self.bot.version) < parse_version(latest.version): + if self.bot.version < parse_version(latest.version): footer = f"A newer version is available v{latest.version}" else: footer = "You are up to date with the latest version." embed.add_field( - name="GitHub", value="https://github.com/kyb3r/modmail", inline=False - ) - - embed.add_field( - name="Discord Server", value="https://discord.gg/F34cRU8", inline=False + name="Want Modmail in Your Server?", + value="Installation guide on GitHub (https://github.com/kyb3r/modmail) " + "and join our discord server (https://discord.gg/F34cRU8)!", inline=False ) embed.add_field( - name="Donate", - value="Support this bot on [`Patreon`](https://patreon.com/kyber).", + name="Support the Developers", + value="This bot is completely free for everyone. We rely on kind individuals " + "like you to support us on [`Patreon`](https://patreon.com/kyber) (includes perks!) " + "to keep this bot free forever!", ) embed.set_footer(text=footer) @@ -648,12 +641,11 @@ async def loop_presence(self): """Set presence to the configured value every 45 minutes.""" # TODO: Does this even work? presence = await self.set_presence() - logger.debug(f'{presence["activity"][1]} {presence["status"][1]}') + logger.debug('Loop... %s - %s', presence["activity"][1], presence["status"][1]) @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() - logger.debug("Starting metadata loop.") logger.line() presence = await self.set_presence() logger.info(presence["activity"][1]) @@ -771,17 +763,15 @@ async def config_set(self, ctx, key: str.lower, *, value: str): if key in keys: try: - value, value_text = await self.bot.config.clean_data(key, value) - except InvalidConfigError as exc: - embed = exc.embed - else: - self.bot.config[key] = value + self.bot.config.set(key, value) await self.bot.config.update() embed = Embed( title="Success", color=self.bot.main_color, - description=f"Set `{key}` to `{value_text}`", + description=f"Set `{key}` to `{self.bot.config[key]}`.", ) + except InvalidConfigError as exc: + embed = exc.embed else: embed = Embed( title="Error", @@ -795,7 +785,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): @config.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.OWNER) - async def config_remove(self, ctx, key: str.lower): + async def config_remove(self, ctx, *, key: str.lower): """Delete a set configuration variable.""" keys = self.bot.config.public_keys if key in keys: @@ -819,7 +809,7 @@ async def config_remove(self, ctx, key: str.lower): @config.command(name="get") @checks.has_permissions(PermissionLevel.OWNER) - async def config_get(self, ctx, key: str.lower = None): + async def config_get(self, ctx, *, key: str.lower = None): """ Show the configuration variables that are currently set. diff --git a/core/_color_data.py b/core/_color_data.py index 997a6a1d25..acfd834505 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -5,41 +5,41 @@ BASE_COLORS = { - "b": "#0000ff", - "g": "#007f00", - "r": "#ff0000", - "c": "#00bfbf", - "m": "#bf00bf", - "y": "#bfbf00", - "k": "#000000", - "w": "#ffffff", + "b": "0000ff", + "g": "007f00", + "r": "ff0000", + "c": "00bfbf", + "m": "bf00bf", + "y": "bfbf00", + "k": "000000", + "w": "ffffff", } # Discord native colors DISCORD_COLORS = { - "default": "#000000", - "teal": "#1abc9c", - "dark teal": "#11806a", - "green": "#2ecc71", - "dark green": "#1f8b4c", - "blue": "#3498db", - "dark blue": "#206694", - "purple": "#9b59b6", - "dark purple": "#71368a", - "magenta": "#e91e63", - "dark magenta": "#ad1457", - "gold": "#f1c40f", - "dark gold": "#c27c0e", - "orange": "#e67e22", - "dark orange": "#a84300", - "red": "#e74c3c", - "dark red": "#992d22", - "lighter gray": "#95a5a6", - "darker gray": "#546e7a", - "light gray": "#979c9f", - "dark gray": "#607d8b", - "blurple": "#7289da", - "grayple": "#99aab5" + "default": "000000", + "teal": "1abc9c", + "dark teal": "11806a", + "green": "2ecc71", + "dark green": "1f8b4c", + "blue": "3498db", + "dark blue": "206694", + "purple": "9b59b6", + "dark purple": "71368a", + "magenta": "e91e63", + "dark magenta": "ad1457", + "gold": "f1c40f", + "dark gold": "c27c0e", + "orange": "e67e22", + "dark orange": "a84300", + "red": "e74c3c", + "dark red": "992d22", + "lighter gray": "95a5a6", + "darker gray": "546e7a", + "light gray": "979c9f", + "dark gray": "607d8b", + "blurple": "7289da", + "grayple": "99aab5" } # Normalize name to "discord:" to avoid name collisions. @@ -48,16 +48,16 @@ # These colors are from Tableau TABLEAU_COLORS = { - "blue": "#1f77b4", - "orange": "#ff7f0e", - "green": "#2ca02c", - "red": "#d62728", - "purple": "#9467bd", - "brown": "#8c564b", - "pink": "#e377c2", - "gray": "#7f7f7f", - "olive": "#bcbd22", - "cyan": "#17becf", + "blue": "1f77b4", + "orange": "ff7f0e", + "green": "2ca02c", + "red": "d62728", + "purple": "9467bd", + "brown": "8c564b", + "pink": "e377c2", + "gray": "7f7f7f", + "olive": "bcbd22", + "cyan": "17becf", } # Normalize name to "tab:" to avoid name collisions. @@ -72,955 +72,955 @@ # # License: http://creativecommons.org/publicdomain/zero/1.0/ XKCD_COLORS = { - "cloudy blue": "#acc2d9", - "dark pastel green": "#56ae57", - "dust": "#b2996e", - "electric lime": "#a8ff04", - "fresh green": "#69d84f", - "light eggplant": "#894585", - "nasty green": "#70b23f", - "really light blue": "#d4ffff", - "tea": "#65ab7c", - "warm purple": "#952e8f", - "yellowish tan": "#fcfc81", - "cement": "#a5a391", - "dark grass green": "#388004", - "dusty teal": "#4c9085", - "grey teal": "#5e9b8a", - "macaroni and cheese": "#efb435", - "pinkish tan": "#d99b82", - "spruce": "#0a5f38", - "strong blue": "#0c06f7", - "toxic green": "#61de2a", - "windows blue": "#3778bf", - "blue blue": "#2242c7", - "blue with a hint of purple": "#533cc6", - "booger": "#9bb53c", - "bright sea green": "#05ffa6", - "dark green blue": "#1f6357", - "deep turquoise": "#017374", - "green teal": "#0cb577", - "strong pink": "#ff0789", - "bland": "#afa88b", - "deep aqua": "#08787f", - "lavender pink": "#dd85d7", - "light moss green": "#a6c875", - "light seafoam green": "#a7ffb5", - "olive yellow": "#c2b709", - "pig pink": "#e78ea5", - "deep lilac": "#966ebd", - "desert": "#ccad60", - "dusty lavender": "#ac86a8", - "purpley grey": "#947e94", - "purply": "#983fb2", - "candy pink": "#ff63e9", - "light pastel green": "#b2fba5", - "boring green": "#63b365", - "kiwi green": "#8ee53f", - "light grey green": "#b7e1a1", - "orange pink": "#ff6f52", - "tea green": "#bdf8a3", - "very light brown": "#d3b683", - "egg shell": "#fffcc4", - "eggplant purple": "#430541", - "powder pink": "#ffb2d0", - "reddish grey": "#997570", - "baby shit brown": "#ad900d", - "liliac": "#c48efd", - "stormy blue": "#507b9c", - "ugly brown": "#7d7103", - "custard": "#fffd78", - "darkish pink": "#da467d", - "deep brown": "#410200", - "greenish beige": "#c9d179", - "manilla": "#fffa86", - "off blue": "#5684ae", - "battleship grey": "#6b7c85", - "browny green": "#6f6c0a", - "bruise": "#7e4071", - "kelley green": "#009337", - "sickly yellow": "#d0e429", - "sunny yellow": "#fff917", - "azul": "#1d5dec", - "darkgreen": "#054907", - "green/yellow": "#b5ce08", - "lichen": "#8fb67b", - "light light green": "#c8ffb0", - "pale gold": "#fdde6c", - "sun yellow": "#ffdf22", - "tan green": "#a9be70", - "burple": "#6832e3", - "butterscotch": "#fdb147", - "toupe": "#c7ac7d", - "dark cream": "#fff39a", - "indian red": "#850e04", - "light lavendar": "#efc0fe", - "poison green": "#40fd14", - "baby puke green": "#b6c406", - "bright yellow green": "#9dff00", - "charcoal grey": "#3c4142", - "squash": "#f2ab15", - "cinnamon": "#ac4f06", - "light pea green": "#c4fe82", - "radioactive green": "#2cfa1f", - "raw sienna": "#9a6200", - "baby purple": "#ca9bf7", - "cocoa": "#875f42", - "light royal blue": "#3a2efe", - "orangeish": "#fd8d49", - "rust brown": "#8b3103", - "sand brown": "#cba560", - "swamp": "#698339", - "tealish green": "#0cdc73", - "burnt siena": "#b75203", - "camo": "#7f8f4e", - "dusk blue": "#26538d", - "fern": "#63a950", - "old rose": "#c87f89", - "pale light green": "#b1fc99", - "peachy pink": "#ff9a8a", - "rosy pink": "#f6688e", - "light bluish green": "#76fda8", - "light bright green": "#53fe5c", - "light neon green": "#4efd54", - "light seafoam": "#a0febf", - "tiffany blue": "#7bf2da", - "washed out green": "#bcf5a6", - "browny orange": "#ca6b02", - "nice blue": "#107ab0", - "sapphire": "#2138ab", - "greyish teal": "#719f91", - "orangey yellow": "#fdb915", - "parchment": "#fefcaf", - "straw": "#fcf679", - "very dark brown": "#1d0200", - "terracota": "#cb6843", - "ugly blue": "#31668a", - "clear blue": "#247afd", - "creme": "#ffffb6", - "foam green": "#90fda9", - "grey/green": "#86a17d", - "light gold": "#fddc5c", - "seafoam blue": "#78d1b6", - "topaz": "#13bbaf", - "violet pink": "#fb5ffc", - "wintergreen": "#20f986", - "yellow tan": "#ffe36e", - "dark fuchsia": "#9d0759", - "indigo blue": "#3a18b1", - "light yellowish green": "#c2ff89", - "pale magenta": "#d767ad", - "rich purple": "#720058", - "sunflower yellow": "#ffda03", - "green/blue": "#01c08d", - "leather": "#ac7434", - "racing green": "#014600", - "vivid purple": "#9900fa", - "dark royal blue": "#02066f", - "hazel": "#8e7618", - "muted pink": "#d1768f", - "booger green": "#96b403", - "canary": "#fdff63", - "cool grey": "#95a3a6", - "dark taupe": "#7f684e", - "darkish purple": "#751973", - "true green": "#089404", - "coral pink": "#ff6163", - "dark sage": "#598556", - "dark slate blue": "#214761", - "flat blue": "#3c73a8", - "mushroom": "#ba9e88", - "rich blue": "#021bf9", - "dirty purple": "#734a65", - "greenblue": "#23c48b", - "icky green": "#8fae22", - "light khaki": "#e6f2a2", - "warm blue": "#4b57db", - "dark hot pink": "#d90166", - "deep sea blue": "#015482", - "carmine": "#9d0216", - "dark yellow green": "#728f02", - "pale peach": "#ffe5ad", - "plum purple": "#4e0550", - "golden rod": "#f9bc08", - "neon red": "#ff073a", - "old pink": "#c77986", - "very pale blue": "#d6fffe", - "blood orange": "#fe4b03", - "grapefruit": "#fd5956", - "sand yellow": "#fce166", - "clay brown": "#b2713d", - "dark blue grey": "#1f3b4d", - "flat green": "#699d4c", - "light green blue": "#56fca2", - "warm pink": "#fb5581", - "dodger blue": "#3e82fc", - "gross green": "#a0bf16", - "ice": "#d6fffa", - "metallic blue": "#4f738e", - "pale salmon": "#ffb19a", - "sap green": "#5c8b15", - "algae": "#54ac68", - "bluey grey": "#89a0b0", - "greeny grey": "#7ea07a", - "highlighter green": "#1bfc06", - "light light blue": "#cafffb", - "light mint": "#b6ffbb", - "raw umber": "#a75e09", - "vivid blue": "#152eff", - "deep lavender": "#8d5eb7", - "dull teal": "#5f9e8f", - "light greenish blue": "#63f7b4", - "mud green": "#606602", - "pinky": "#fc86aa", - "red wine": "#8c0034", - "shit green": "#758000", - "tan brown": "#ab7e4c", - "darkblue": "#030764", - "rosa": "#fe86a4", - "lipstick": "#d5174e", - "pale mauve": "#fed0fc", - "claret": "#680018", - "dandelion": "#fedf08", - "orangered": "#fe420f", - "poop green": "#6f7c00", - "ruby": "#ca0147", - "dark": "#1b2431", - "greenish turquoise": "#00fbb0", - "pastel red": "#db5856", - "piss yellow": "#ddd618", - "bright cyan": "#41fdfe", - "dark coral": "#cf524e", - "algae green": "#21c36f", - "darkish red": "#a90308", - "reddy brown": "#6e1005", - "blush pink": "#fe828c", - "camouflage green": "#4b6113", - "lawn green": "#4da409", - "putty": "#beae8a", - "vibrant blue": "#0339f8", - "dark sand": "#a88f59", - "purple/blue": "#5d21d0", - "saffron": "#feb209", - "twilight": "#4e518b", - "warm brown": "#964e02", - "bluegrey": "#85a3b2", - "bubble gum pink": "#ff69af", - "duck egg blue": "#c3fbf4", - "greenish cyan": "#2afeb7", - "petrol": "#005f6a", - "royal": "#0c1793", - "butter": "#ffff81", - "dusty orange": "#f0833a", - "off yellow": "#f1f33f", - "pale olive green": "#b1d27b", - "orangish": "#fc824a", - "leaf": "#71aa34", - "light blue grey": "#b7c9e2", - "dried blood": "#4b0101", - "lightish purple": "#a552e6", - "rusty red": "#af2f0d", - "lavender blue": "#8b88f8", - "light grass green": "#9af764", - "light mint green": "#a6fbb2", - "sunflower": "#ffc512", - "velvet": "#750851", - "brick orange": "#c14a09", - "lightish red": "#fe2f4a", - "pure blue": "#0203e2", - "twilight blue": "#0a437a", - "violet red": "#a50055", - "yellowy brown": "#ae8b0c", - "carnation": "#fd798f", - "muddy yellow": "#bfac05", - "dark seafoam green": "#3eaf76", - "deep rose": "#c74767", - "dusty red": "#b9484e", - "grey/blue": "#647d8e", - "lemon lime": "#bffe28", - "purple/pink": "#d725de", - "brown yellow": "#b29705", - "purple brown": "#673a3f", - "wisteria": "#a87dc2", - "banana yellow": "#fafe4b", - "lipstick red": "#c0022f", - "water blue": "#0e87cc", - "brown grey": "#8d8468", - "vibrant purple": "#ad03de", - "baby green": "#8cff9e", - "barf green": "#94ac02", - "eggshell blue": "#c4fff7", - "sandy yellow": "#fdee73", - "cool green": "#33b864", - "pale": "#fff9d0", - "blue/grey": "#758da3", - "hot magenta": "#f504c9", - "greyblue": "#77a1b5", - "purpley": "#8756e4", - "baby shit green": "#889717", - "brownish pink": "#c27e79", - "dark aquamarine": "#017371", - "diarrhea": "#9f8303", - "light mustard": "#f7d560", - "pale sky blue": "#bdf6fe", - "turtle green": "#75b84f", - "bright olive": "#9cbb04", - "dark grey blue": "#29465b", - "greeny brown": "#696006", - "lemon green": "#adf802", - "light periwinkle": "#c1c6fc", - "seaweed green": "#35ad6b", - "sunshine yellow": "#fffd37", - "ugly purple": "#a442a0", - "medium pink": "#f36196", - "puke brown": "#947706", - "very light pink": "#fff4f2", - "viridian": "#1e9167", - "bile": "#b5c306", - "faded yellow": "#feff7f", - "very pale green": "#cffdbc", - "vibrant green": "#0add08", - "bright lime": "#87fd05", - "spearmint": "#1ef876", - "light aquamarine": "#7bfdc7", - "light sage": "#bcecac", - "yellowgreen": "#bbf90f", - "baby poo": "#ab9004", - "dark seafoam": "#1fb57a", - "deep teal": "#00555a", - "heather": "#a484ac", - "rust orange": "#c45508", - "dirty blue": "#3f829d", - "fern green": "#548d44", - "bright lilac": "#c95efb", - "weird green": "#3ae57f", - "peacock blue": "#016795", - "avocado green": "#87a922", - "faded orange": "#f0944d", - "grape purple": "#5d1451", - "hot green": "#25ff29", - "lime yellow": "#d0fe1d", - "mango": "#ffa62b", - "shamrock": "#01b44c", - "bubblegum": "#ff6cb5", - "purplish brown": "#6b4247", - "vomit yellow": "#c7c10c", - "pale cyan": "#b7fffa", - "key lime": "#aeff6e", - "tomato red": "#ec2d01", - "lightgreen": "#76ff7b", - "merlot": "#730039", - "night blue": "#040348", - "purpleish pink": "#df4ec8", - "apple": "#6ecb3c", - "baby poop green": "#8f9805", - "green apple": "#5edc1f", - "heliotrope": "#d94ff5", - "yellow/green": "#c8fd3d", - "almost black": "#070d0d", - "cool blue": "#4984b8", - "leafy green": "#51b73b", - "mustard brown": "#ac7e04", - "dusk": "#4e5481", - "dull brown": "#876e4b", - "frog green": "#58bc08", - "vivid green": "#2fef10", - "bright light green": "#2dfe54", - "fluro green": "#0aff02", - "kiwi": "#9cef43", - "seaweed": "#18d17b", - "navy green": "#35530a", - "ultramarine blue": "#1805db", - "iris": "#6258c4", - "pastel orange": "#ff964f", - "yellowish orange": "#ffab0f", - "perrywinkle": "#8f8ce7", - "tealish": "#24bca8", - "dark plum": "#3f012c", - "pear": "#cbf85f", - "pinkish orange": "#ff724c", - "midnight purple": "#280137", - "light urple": "#b36ff6", - "dark mint": "#48c072", - "greenish tan": "#bccb7a", - "light burgundy": "#a8415b", - "turquoise blue": "#06b1c4", - "ugly pink": "#cd7584", - "sandy": "#f1da7a", - "electric pink": "#ff0490", - "muted purple": "#805b87", - "mid green": "#50a747", - "greyish": "#a8a495", - "neon yellow": "#cfff04", - "banana": "#ffff7e", - "carnation pink": "#ff7fa7", - "tomato": "#ef4026", - "sea": "#3c9992", - "muddy brown": "#886806", - "turquoise green": "#04f489", - "buff": "#fef69e", - "fawn": "#cfaf7b", - "muted blue": "#3b719f", - "pale rose": "#fdc1c5", - "dark mint green": "#20c073", - "amethyst": "#9b5fc0", - "blue/green": "#0f9b8e", - "chestnut": "#742802", - "sick green": "#9db92c", - "pea": "#a4bf20", - "rusty orange": "#cd5909", - "stone": "#ada587", - "rose red": "#be013c", - "pale aqua": "#b8ffeb", - "deep orange": "#dc4d01", - "earth": "#a2653e", - "mossy green": "#638b27", - "grassy green": "#419c03", - "pale lime green": "#b1ff65", - "light grey blue": "#9dbcd4", - "pale grey": "#fdfdfe", - "asparagus": "#77ab56", - "blueberry": "#464196", - "purple red": "#990147", - "pale lime": "#befd73", - "greenish teal": "#32bf84", - "caramel": "#af6f09", - "deep magenta": "#a0025c", - "light peach": "#ffd8b1", - "milk chocolate": "#7f4e1e", - "ocher": "#bf9b0c", - "off green": "#6ba353", - "purply pink": "#f075e6", - "lightblue": "#7bc8f6", - "dusky blue": "#475f94", - "golden": "#f5bf03", - "light beige": "#fffeb6", - "butter yellow": "#fffd74", - "dusky purple": "#895b7b", - "french blue": "#436bad", - "ugly yellow": "#d0c101", - "greeny yellow": "#c6f808", - "orangish red": "#f43605", - "shamrock green": "#02c14d", - "orangish brown": "#b25f03", - "tree green": "#2a7e19", - "deep violet": "#490648", - "gunmetal": "#536267", - "blue/purple": "#5a06ef", - "cherry": "#cf0234", - "sandy brown": "#c4a661", - "warm grey": "#978a84", - "dark indigo": "#1f0954", - "midnight": "#03012d", - "bluey green": "#2bb179", - "grey pink": "#c3909b", - "soft purple": "#a66fb5", - "blood": "#770001", - "brown red": "#922b05", - "medium grey": "#7d7f7c", - "berry": "#990f4b", - "poo": "#8f7303", - "purpley pink": "#c83cb9", - "light salmon": "#fea993", - "snot": "#acbb0d", - "easter purple": "#c071fe", - "light yellow green": "#ccfd7f", - "dark navy blue": "#00022e", - "drab": "#828344", - "light rose": "#ffc5cb", - "rouge": "#ab1239", - "purplish red": "#b0054b", - "slime green": "#99cc04", - "baby poop": "#937c00", - "irish green": "#019529", - "pink/purple": "#ef1de7", - "dark navy": "#000435", - "greeny blue": "#42b395", - "light plum": "#9d5783", - "pinkish grey": "#c8aca9", - "dirty orange": "#c87606", - "rust red": "#aa2704", - "pale lilac": "#e4cbff", - "orangey red": "#fa4224", - "primary blue": "#0804f9", - "kermit green": "#5cb200", - "brownish purple": "#76424e", - "murky green": "#6c7a0e", - "wheat": "#fbdd7e", - "very dark purple": "#2a0134", - "bottle green": "#044a05", - "watermelon": "#fd4659", - "deep sky blue": "#0d75f8", - "fire engine red": "#fe0002", - "yellow ochre": "#cb9d06", - "pumpkin orange": "#fb7d07", - "pale olive": "#b9cc81", - "light lilac": "#edc8ff", - "lightish green": "#61e160", - "carolina blue": "#8ab8fe", - "mulberry": "#920a4e", - "shocking pink": "#fe02a2", - "auburn": "#9a3001", - "bright lime green": "#65fe08", - "celadon": "#befdb7", - "pinkish brown": "#b17261", - "poo brown": "#885f01", - "bright sky blue": "#02ccfe", - "celery": "#c1fd95", - "dirt brown": "#836539", - "strawberry": "#fb2943", - "dark lime": "#84b701", - "copper": "#b66325", - "medium brown": "#7f5112", - "muted green": "#5fa052", - "robin's egg": "#6dedfd", - "bright aqua": "#0bf9ea", - "bright lavender": "#c760ff", - "ivory": "#ffffcb", - "very light purple": "#f6cefc", - "light navy": "#155084", - "pink red": "#f5054f", - "olive brown": "#645403", - "poop brown": "#7a5901", - "mustard green": "#a8b504", - "ocean green": "#3d9973", - "very dark blue": "#000133", - "dusty green": "#76a973", - "light navy blue": "#2e5a88", - "minty green": "#0bf77d", - "adobe": "#bd6c48", - "barney": "#ac1db8", - "jade green": "#2baf6a", - "bright light blue": "#26f7fd", - "light lime": "#aefd6c", - "dark khaki": "#9b8f55", - "orange yellow": "#ffad01", - "ocre": "#c69c04", - "maize": "#f4d054", - "faded pink": "#de9dac", - "british racing green": "#05480d", - "sandstone": "#c9ae74", - "mud brown": "#60460f", - "light sea green": "#98f6b0", - "robin egg blue": "#8af1fe", - "aqua marine": "#2ee8bb", - "dark sea green": "#11875d", - "soft pink": "#fdb0c0", - "orangey brown": "#b16002", - "cherry red": "#f7022a", - "burnt yellow": "#d5ab09", - "brownish grey": "#86775f", - "camel": "#c69f59", - "purplish grey": "#7a687f", - "marine": "#042e60", - "greyish pink": "#c88d94", - "pale turquoise": "#a5fbd5", - "pastel yellow": "#fffe71", - "bluey purple": "#6241c7", - "canary yellow": "#fffe40", - "faded red": "#d3494e", - "sepia": "#985e2b", - "coffee": "#a6814c", - "bright magenta": "#ff08e8", - "mocha": "#9d7651", - "ecru": "#feffca", - "purpleish": "#98568d", - "cranberry": "#9e003a", - "darkish green": "#287c37", - "brown orange": "#b96902", - "dusky rose": "#ba6873", - "melon": "#ff7855", - "sickly green": "#94b21c", - "silver": "#c5c9c7", - "purply blue": "#661aee", - "purpleish blue": "#6140ef", - "hospital green": "#9be5aa", - "shit brown": "#7b5804", - "mid blue": "#276ab3", - "amber": "#feb308", - "easter green": "#8cfd7e", - "soft blue": "#6488ea", - "cerulean blue": "#056eee", - "golden brown": "#b27a01", - "bright turquoise": "#0ffef9", - "red pink": "#fa2a55", - "red purple": "#820747", - "greyish brown": "#7a6a4f", - "vermillion": "#f4320c", - "russet": "#a13905", - "steel grey": "#6f828a", - "lighter purple": "#a55af4", - "bright violet": "#ad0afd", - "prussian blue": "#004577", - "slate green": "#658d6d", - "dirty pink": "#ca7b80", - "dark blue green": "#005249", - "pine": "#2b5d34", - "yellowy green": "#bff128", - "dark gold": "#b59410", - "bluish": "#2976bb", - "darkish blue": "#014182", - "dull red": "#bb3f3f", - "pinky red": "#fc2647", - "bronze": "#a87900", - "pale teal": "#82cbb2", - "military green": "#667c3e", - "barbie pink": "#fe46a5", - "bubblegum pink": "#fe83cc", - "pea soup green": "#94a617", - "dark mustard": "#a88905", - "shit": "#7f5f00", - "medium purple": "#9e43a2", - "very dark green": "#062e03", - "dirt": "#8a6e45", - "dusky pink": "#cc7a8b", - "red violet": "#9e0168", - "lemon yellow": "#fdff38", - "pistachio": "#c0fa8b", - "dull yellow": "#eedc5b", - "dark lime green": "#7ebd01", - "denim blue": "#3b5b92", - "teal blue": "#01889f", - "lightish blue": "#3d7afd", - "purpley blue": "#5f34e7", - "light indigo": "#6d5acf", - "swamp green": "#748500", - "brown green": "#706c11", - "dark maroon": "#3c0008", - "hot purple": "#cb00f5", - "dark forest green": "#002d04", - "faded blue": "#658cbb", - "drab green": "#749551", - "light lime green": "#b9ff66", - "snot green": "#9dc100", - "yellowish": "#faee66", - "light blue green": "#7efbb3", - "bordeaux": "#7b002c", - "light mauve": "#c292a1", - "ocean": "#017b92", - "marigold": "#fcc006", - "muddy green": "#657432", - "dull orange": "#d8863b", - "steel": "#738595", - "electric purple": "#aa23ff", - "fluorescent green": "#08ff08", - "yellowish brown": "#9b7a01", - "blush": "#f29e8e", - "soft green": "#6fc276", - "bright orange": "#ff5b00", - "lemon": "#fdff52", - "purple grey": "#866f85", - "acid green": "#8ffe09", - "pale lavender": "#eecffe", - "violet blue": "#510ac9", - "light forest green": "#4f9153", - "burnt red": "#9f2305", - "khaki green": "#728639", - "cerise": "#de0c62", - "faded purple": "#916e99", - "apricot": "#ffb16d", - "dark olive green": "#3c4d03", - "grey brown": "#7f7053", - "green grey": "#77926f", - "true blue": "#010fcc", - "pale violet": "#ceaefa", - "periwinkle blue": "#8f99fb", - "light sky blue": "#c6fcff", - "blurple": "#5539cc", - "green brown": "#544e03", - "bluegreen": "#017a79", - "bright teal": "#01f9c6", - "brownish yellow": "#c9b003", - "pea soup": "#929901", - "forest": "#0b5509", - "barney purple": "#a00498", - "ultramarine": "#2000b1", - "purplish": "#94568c", - "puke yellow": "#c2be0e", - "bluish grey": "#748b97", - "dark periwinkle": "#665fd1", - "dark lilac": "#9c6da5", - "reddish": "#c44240", - "light maroon": "#a24857", - "dusty purple": "#825f87", - "terra cotta": "#c9643b", - "avocado": "#90b134", - "marine blue": "#01386a", - "teal green": "#25a36f", - "slate grey": "#59656d", - "lighter green": "#75fd63", - "electric green": "#21fc0d", - "dusty blue": "#5a86ad", - "golden yellow": "#fec615", - "bright yellow": "#fffd01", - "light lavender": "#dfc5fe", - "umber": "#b26400", - "poop": "#7f5e00", - "dark peach": "#de7e5d", - "jungle green": "#048243", - "eggshell": "#ffffd4", - "denim": "#3b638c", - "yellow brown": "#b79400", - "dull purple": "#84597e", - "chocolate brown": "#411900", - "wine red": "#7b0323", - "neon blue": "#04d9ff", - "dirty green": "#667e2c", - "light tan": "#fbeeac", - "ice blue": "#d7fffe", - "cadet blue": "#4e7496", - "dark mauve": "#874c62", - "very light blue": "#d5ffff", - "grey purple": "#826d8c", - "pastel pink": "#ffbacd", - "very light green": "#d1ffbd", - "dark sky blue": "#448ee4", - "evergreen": "#05472a", - "dull pink": "#d5869d", - "aubergine": "#3d0734", - "mahogany": "#4a0100", - "reddish orange": "#f8481c", - "deep green": "#02590f", - "vomit green": "#89a203", - "purple pink": "#e03fd8", - "dusty pink": "#d58a94", - "faded green": "#7bb274", - "camo green": "#526525", - "pinky purple": "#c94cbe", - "pink purple": "#db4bda", - "brownish red": "#9e3623", - "dark rose": "#b5485d", - "mud": "#735c12", - "brownish": "#9c6d57", - "emerald green": "#028f1e", - "pale brown": "#b1916e", - "dull blue": "#49759c", - "burnt umber": "#a0450e", - "medium green": "#39ad48", - "clay": "#b66a50", - "light aqua": "#8cffdb", - "light olive green": "#a4be5c", - "brownish orange": "#cb7723", - "dark aqua": "#05696b", - "purplish pink": "#ce5dae", - "dark salmon": "#c85a53", - "greenish grey": "#96ae8d", - "jade": "#1fa774", - "ugly green": "#7a9703", - "dark beige": "#ac9362", - "emerald": "#01a049", - "pale red": "#d9544d", - "light magenta": "#fa5ff7", - "sky": "#82cafc", - "light cyan": "#acfffc", - "yellow orange": "#fcb001", - "reddish purple": "#910951", - "reddish pink": "#fe2c54", - "orchid": "#c875c4", - "dirty yellow": "#cdc50a", - "orange red": "#fd411e", - "deep red": "#9a0200", - "orange brown": "#be6400", - "cobalt blue": "#030aa7", - "neon pink": "#fe019a", - "rose pink": "#f7879a", - "greyish purple": "#887191", - "raspberry": "#b00149", - "aqua green": "#12e193", - "salmon pink": "#fe7b7c", - "tangerine": "#ff9408", - "brownish green": "#6a6e09", - "red brown": "#8b2e16", - "greenish brown": "#696112", - "pumpkin": "#e17701", - "pine green": "#0a481e", - "charcoal": "#343837", - "baby pink": "#ffb7ce", - "cornflower": "#6a79f7", - "blue violet": "#5d06e9", - "chocolate": "#3d1c02", - "greyish green": "#82a67d", - "scarlet": "#be0119", - "green yellow": "#c9ff27", - "dark olive": "#373e02", - "sienna": "#a9561e", - "pastel purple": "#caa0ff", - "terracotta": "#ca6641", - "aqua blue": "#02d8e9", - "sage green": "#88b378", - "blood red": "#980002", - "deep pink": "#cb0162", - "grass": "#5cac2d", - "moss": "#769958", - "pastel blue": "#a2bffe", - "bluish green": "#10a674", - "green blue": "#06b48b", - "dark tan": "#af884a", - "greenish blue": "#0b8b87", - "pale orange": "#ffa756", - "vomit": "#a2a415", - "forrest green": "#154406", - "dark lavender": "#856798", - "dark violet": "#34013f", - "purple blue": "#632de9", - "dark cyan": "#0a888a", - "olive drab": "#6f7632", - "pinkish": "#d46a7e", - "cobalt": "#1e488f", - "neon purple": "#bc13fe", - "light turquoise": "#7ef4cc", - "apple green": "#76cd26", - "dull green": "#74a662", - "wine": "#80013f", - "powder blue": "#b1d1fc", - "off white": "#ffffe4", - "electric blue": "#0652ff", - "dark turquoise": "#045c5a", - "blue purple": "#5729ce", - "azure": "#069af3", - "bright red": "#ff000d", - "pinkish red": "#f10c45", - "cornflower blue": "#5170d7", - "light olive": "#acbf69", - "grape": "#6c3461", - "greyish blue": "#5e819d", - "purplish blue": "#601ef9", - "yellowish green": "#b0dd16", - "greenish yellow": "#cdfd02", - "medium blue": "#2c6fbb", - "dusty rose": "#c0737a", - "light violet": "#d6b4fc", - "midnight blue": "#020035", - "bluish purple": "#703be7", - "red orange": "#fd3c06", - "dark magenta": "#960056", - "greenish": "#40a368", - "ocean blue": "#03719c", - "coral": "#fc5a50", - "cream": "#ffffc2", - "reddish brown": "#7f2b0a", - "burnt sienna": "#b04e0f", - "brick": "#a03623", - "sage": "#87ae73", - "grey green": "#789b73", - "white": "#ffffff", - "robin's egg blue": "#98eff9", - "moss green": "#658b38", - "steel blue": "#5a7d9a", - "eggplant": "#380835", - "light yellow": "#fffe7a", - "leaf green": "#5ca904", - "light grey": "#d8dcd6", - "puke": "#a5a502", - "pinkish purple": "#d648d7", - "sea blue": "#047495", - "pale purple": "#b790d4", - "slate blue": "#5b7c99", - "blue grey": "#607c8e", - "hunter green": "#0b4008", - "fuchsia": "#ed0dd9", - "crimson": "#8c000f", - "pale yellow": "#ffff84", - "ochre": "#bf9005", - "mustard yellow": "#d2bd0a", - "light red": "#ff474c", - "cerulean": "#0485d1", - "pale pink": "#ffcfdc", - "deep blue": "#040273", - "rust": "#a83c09", - "light teal": "#90e4c1", - "slate": "#516572", - "goldenrod": "#fac205", - "dark yellow": "#d5b60a", - "dark grey": "#363737", - "army green": "#4b5d16", - "grey blue": "#6b8ba4", - "seafoam": "#80f9ad", - "puce": "#a57e52", - "spring green": "#a9f971", - "dark orange": "#c65102", - "sand": "#e2ca76", - "pastel green": "#b0ff9d", - "mint": "#9ffeb0", - "light orange": "#fdaa48", - "bright pink": "#fe01b1", - "chartreuse": "#c1f80a", - "deep purple": "#36013f", - "dark brown": "#341c02", - "taupe": "#b9a281", - "pea green": "#8eab12", - "puke green": "#9aae07", - "kelly green": "#02ab2e", - "seafoam green": "#7af9ab", - "blue green": "#137e6d", - "khaki": "#aaa662", - "burgundy": "#610023", - "dark teal": "#014d4e", - "brick red": "#8f1402", - "royal purple": "#4b006e", - "plum": "#580f41", - "mint green": "#8fff9f", - "gold": "#dbb40c", - "baby blue": "#a2cffe", - "yellow green": "#c0fb2d", - "bright purple": "#be03fd", - "dark red": "#840000", - "pale blue": "#d0fefe", - "grass green": "#3f9b0b", - "navy": "#01153e", - "aquamarine": "#04d8b2", - "burnt orange": "#c04e01", - "neon green": "#0cff0c", - "bright blue": "#0165fc", - "rose": "#cf6275", - "light pink": "#ffd1df", - "mustard": "#ceb301", - "indigo": "#380282", - "lime": "#aaff32", - "sea green": "#53fca1", - "periwinkle": "#8e82fe", - "dark pink": "#cb416b", - "olive green": "#677a04", - "peach": "#ffb07c", - "pale green": "#c7fdb5", - "light brown": "#ad8150", - "hot pink": "#ff028d", - "black": "#000000", - "lilac": "#cea2fd", - "navy blue": "#001146", - "royal blue": "#0504aa", - "beige": "#e6daa6", - "salmon": "#ff796c", - "olive": "#6e750e", - "maroon": "#650021", - "bright green": "#01ff07", - "dark purple": "#35063e", - "mauve": "#ae7181", - "forest green": "#06470c", - "aqua": "#13eac9", - "cyan": "#00ffff", - "tan": "#d1b26f", - "dark blue": "#00035b", - "lavender": "#c79fef", - "turquoise": "#06c2ac", - "dark green": "#033500", - "violet": "#9a0eea", - "light purple": "#bf77f6", - "lime green": "#89fe05", - "grey": "#929591", - "sky blue": "#75bbfd", - "yellow": "#ffff14", - "magenta": "#c20078", - "light green": "#96f97b", - "orange": "#f97306", - "teal": "#029386", - "light blue": "#95d0fc", - "red": "#e50000", - "brown": "#653700", - "pink": "#ff81c0", - "blue": "#0343df", - "green": "#15b01a", - "purple": "#7e1e9c", + "cloudy blue": "acc2d9", + "dark pastel green": "56ae57", + "dust": "b2996e", + "electric lime": "a8ff04", + "fresh green": "69d84f", + "light eggplant": "894585", + "nasty green": "70b23f", + "really light blue": "d4ffff", + "tea": "65ab7c", + "warm purple": "952e8f", + "yellowish tan": "fcfc81", + "cement": "a5a391", + "dark grass green": "388004", + "dusty teal": "4c9085", + "grey teal": "5e9b8a", + "macaroni and cheese": "efb435", + "pinkish tan": "d99b82", + "spruce": "0a5f38", + "strong blue": "0c06f7", + "toxic green": "61de2a", + "windows blue": "3778bf", + "blue blue": "2242c7", + "blue with a hint of purple": "533cc6", + "booger": "9bb53c", + "bright sea green": "05ffa6", + "dark green blue": "1f6357", + "deep turquoise": "017374", + "green teal": "0cb577", + "strong pink": "ff0789", + "bland": "afa88b", + "deep aqua": "08787f", + "lavender pink": "dd85d7", + "light moss green": "a6c875", + "light seafoam green": "a7ffb5", + "olive yellow": "c2b709", + "pig pink": "e78ea5", + "deep lilac": "966ebd", + "desert": "ccad60", + "dusty lavender": "ac86a8", + "purpley grey": "947e94", + "purply": "983fb2", + "candy pink": "ff63e9", + "light pastel green": "b2fba5", + "boring green": "63b365", + "kiwi green": "8ee53f", + "light grey green": "b7e1a1", + "orange pink": "ff6f52", + "tea green": "bdf8a3", + "very light brown": "d3b683", + "egg shell": "fffcc4", + "eggplant purple": "430541", + "powder pink": "ffb2d0", + "reddish grey": "997570", + "baby shit brown": "ad900d", + "liliac": "c48efd", + "stormy blue": "507b9c", + "ugly brown": "7d7103", + "custard": "fffd78", + "darkish pink": "da467d", + "deep brown": "410200", + "greenish beige": "c9d179", + "manilla": "fffa86", + "off blue": "5684ae", + "battleship grey": "6b7c85", + "browny green": "6f6c0a", + "bruise": "7e4071", + "kelley green": "009337", + "sickly yellow": "d0e429", + "sunny yellow": "fff917", + "azul": "1d5dec", + "darkgreen": "054907", + "green/yellow": "b5ce08", + "lichen": "8fb67b", + "light light green": "c8ffb0", + "pale gold": "fdde6c", + "sun yellow": "ffdf22", + "tan green": "a9be70", + "burple": "6832e3", + "butterscotch": "fdb147", + "toupe": "c7ac7d", + "dark cream": "fff39a", + "indian red": "850e04", + "light lavendar": "efc0fe", + "poison green": "40fd14", + "baby puke green": "b6c406", + "bright yellow green": "9dff00", + "charcoal grey": "3c4142", + "squash": "f2ab15", + "cinnamon": "ac4f06", + "light pea green": "c4fe82", + "radioactive green": "2cfa1f", + "raw sienna": "9a6200", + "baby purple": "ca9bf7", + "cocoa": "875f42", + "light royal blue": "3a2efe", + "orangeish": "fd8d49", + "rust brown": "8b3103", + "sand brown": "cba560", + "swamp": "698339", + "tealish green": "0cdc73", + "burnt siena": "b75203", + "camo": "7f8f4e", + "dusk blue": "26538d", + "fern": "63a950", + "old rose": "c87f89", + "pale light green": "b1fc99", + "peachy pink": "ff9a8a", + "rosy pink": "f6688e", + "light bluish green": "76fda8", + "light bright green": "53fe5c", + "light neon green": "4efd54", + "light seafoam": "a0febf", + "tiffany blue": "7bf2da", + "washed out green": "bcf5a6", + "browny orange": "ca6b02", + "nice blue": "107ab0", + "sapphire": "2138ab", + "greyish teal": "719f91", + "orangey yellow": "fdb915", + "parchment": "fefcaf", + "straw": "fcf679", + "very dark brown": "1d0200", + "terracota": "cb6843", + "ugly blue": "31668a", + "clear blue": "247afd", + "creme": "ffffb6", + "foam green": "90fda9", + "grey/green": "86a17d", + "light gold": "fddc5c", + "seafoam blue": "78d1b6", + "topaz": "13bbaf", + "violet pink": "fb5ffc", + "wintergreen": "20f986", + "yellow tan": "ffe36e", + "dark fuchsia": "9d0759", + "indigo blue": "3a18b1", + "light yellowish green": "c2ff89", + "pale magenta": "d767ad", + "rich purple": "720058", + "sunflower yellow": "ffda03", + "green/blue": "01c08d", + "leather": "ac7434", + "racing green": "014600", + "vivid purple": "9900fa", + "dark royal blue": "02066f", + "hazel": "8e7618", + "muted pink": "d1768f", + "booger green": "96b403", + "canary": "fdff63", + "cool grey": "95a3a6", + "dark taupe": "7f684e", + "darkish purple": "751973", + "true green": "089404", + "coral pink": "ff6163", + "dark sage": "598556", + "dark slate blue": "214761", + "flat blue": "3c73a8", + "mushroom": "ba9e88", + "rich blue": "021bf9", + "dirty purple": "734a65", + "greenblue": "23c48b", + "icky green": "8fae22", + "light khaki": "e6f2a2", + "warm blue": "4b57db", + "dark hot pink": "d90166", + "deep sea blue": "015482", + "carmine": "9d0216", + "dark yellow green": "728f02", + "pale peach": "ffe5ad", + "plum purple": "4e0550", + "golden rod": "f9bc08", + "neon red": "ff073a", + "old pink": "c77986", + "very pale blue": "d6fffe", + "blood orange": "fe4b03", + "grapefruit": "fd5956", + "sand yellow": "fce166", + "clay brown": "b2713d", + "dark blue grey": "1f3b4d", + "flat green": "699d4c", + "light green blue": "56fca2", + "warm pink": "fb5581", + "dodger blue": "3e82fc", + "gross green": "a0bf16", + "ice": "d6fffa", + "metallic blue": "4f738e", + "pale salmon": "ffb19a", + "sap green": "5c8b15", + "algae": "54ac68", + "bluey grey": "89a0b0", + "greeny grey": "7ea07a", + "highlighter green": "1bfc06", + "light light blue": "cafffb", + "light mint": "b6ffbb", + "raw umber": "a75e09", + "vivid blue": "152eff", + "deep lavender": "8d5eb7", + "dull teal": "5f9e8f", + "light greenish blue": "63f7b4", + "mud green": "606602", + "pinky": "fc86aa", + "red wine": "8c0034", + "shit green": "758000", + "tan brown": "ab7e4c", + "darkblue": "030764", + "rosa": "fe86a4", + "lipstick": "d5174e", + "pale mauve": "fed0fc", + "claret": "680018", + "dandelion": "fedf08", + "orangered": "fe420f", + "poop green": "6f7c00", + "ruby": "ca0147", + "dark": "1b2431", + "greenish turquoise": "00fbb0", + "pastel red": "db5856", + "piss yellow": "ddd618", + "bright cyan": "41fdfe", + "dark coral": "cf524e", + "algae green": "21c36f", + "darkish red": "a90308", + "reddy brown": "6e1005", + "blush pink": "fe828c", + "camouflage green": "4b6113", + "lawn green": "4da409", + "putty": "beae8a", + "vibrant blue": "0339f8", + "dark sand": "a88f59", + "purple/blue": "5d21d0", + "saffron": "feb209", + "twilight": "4e518b", + "warm brown": "964e02", + "bluegrey": "85a3b2", + "bubble gum pink": "ff69af", + "duck egg blue": "c3fbf4", + "greenish cyan": "2afeb7", + "petrol": "005f6a", + "royal": "0c1793", + "butter": "ffff81", + "dusty orange": "f0833a", + "off yellow": "f1f33f", + "pale olive green": "b1d27b", + "orangish": "fc824a", + "leaf": "71aa34", + "light blue grey": "b7c9e2", + "dried blood": "4b0101", + "lightish purple": "a552e6", + "rusty red": "af2f0d", + "lavender blue": "8b88f8", + "light grass green": "9af764", + "light mint green": "a6fbb2", + "sunflower": "ffc512", + "velvet": "750851", + "brick orange": "c14a09", + "lightish red": "fe2f4a", + "pure blue": "0203e2", + "twilight blue": "0a437a", + "violet red": "a50055", + "yellowy brown": "ae8b0c", + "carnation": "fd798f", + "muddy yellow": "bfac05", + "dark seafoam green": "3eaf76", + "deep rose": "c74767", + "dusty red": "b9484e", + "grey/blue": "647d8e", + "lemon lime": "bffe28", + "purple/pink": "d725de", + "brown yellow": "b29705", + "purple brown": "673a3f", + "wisteria": "a87dc2", + "banana yellow": "fafe4b", + "lipstick red": "c0022f", + "water blue": "0e87cc", + "brown grey": "8d8468", + "vibrant purple": "ad03de", + "baby green": "8cff9e", + "barf green": "94ac02", + "eggshell blue": "c4fff7", + "sandy yellow": "fdee73", + "cool green": "33b864", + "pale": "fff9d0", + "blue/grey": "758da3", + "hot magenta": "f504c9", + "greyblue": "77a1b5", + "purpley": "8756e4", + "baby shit green": "889717", + "brownish pink": "c27e79", + "dark aquamarine": "017371", + "diarrhea": "9f8303", + "light mustard": "f7d560", + "pale sky blue": "bdf6fe", + "turtle green": "75b84f", + "bright olive": "9cbb04", + "dark grey blue": "29465b", + "greeny brown": "696006", + "lemon green": "adf802", + "light periwinkle": "c1c6fc", + "seaweed green": "35ad6b", + "sunshine yellow": "fffd37", + "ugly purple": "a442a0", + "medium pink": "f36196", + "puke brown": "947706", + "very light pink": "fff4f2", + "viridian": "1e9167", + "bile": "b5c306", + "faded yellow": "feff7f", + "very pale green": "cffdbc", + "vibrant green": "0add08", + "bright lime": "87fd05", + "spearmint": "1ef876", + "light aquamarine": "7bfdc7", + "light sage": "bcecac", + "yellowgreen": "bbf90f", + "baby poo": "ab9004", + "dark seafoam": "1fb57a", + "deep teal": "00555a", + "heather": "a484ac", + "rust orange": "c45508", + "dirty blue": "3f829d", + "fern green": "548d44", + "bright lilac": "c95efb", + "weird green": "3ae57f", + "peacock blue": "016795", + "avocado green": "87a922", + "faded orange": "f0944d", + "grape purple": "5d1451", + "hot green": "25ff29", + "lime yellow": "d0fe1d", + "mango": "ffa62b", + "shamrock": "01b44c", + "bubblegum": "ff6cb5", + "purplish brown": "6b4247", + "vomit yellow": "c7c10c", + "pale cyan": "b7fffa", + "key lime": "aeff6e", + "tomato red": "ec2d01", + "lightgreen": "76ff7b", + "merlot": "730039", + "night blue": "040348", + "purpleish pink": "df4ec8", + "apple": "6ecb3c", + "baby poop green": "8f9805", + "green apple": "5edc1f", + "heliotrope": "d94ff5", + "yellow/green": "c8fd3d", + "almost black": "070d0d", + "cool blue": "4984b8", + "leafy green": "51b73b", + "mustard brown": "ac7e04", + "dusk": "4e5481", + "dull brown": "876e4b", + "frog green": "58bc08", + "vivid green": "2fef10", + "bright light green": "2dfe54", + "fluro green": "0aff02", + "kiwi": "9cef43", + "seaweed": "18d17b", + "navy green": "35530a", + "ultramarine blue": "1805db", + "iris": "6258c4", + "pastel orange": "ff964f", + "yellowish orange": "ffab0f", + "perrywinkle": "8f8ce7", + "tealish": "24bca8", + "dark plum": "3f012c", + "pear": "cbf85f", + "pinkish orange": "ff724c", + "midnight purple": "280137", + "light urple": "b36ff6", + "dark mint": "48c072", + "greenish tan": "bccb7a", + "light burgundy": "a8415b", + "turquoise blue": "06b1c4", + "ugly pink": "cd7584", + "sandy": "f1da7a", + "electric pink": "ff0490", + "muted purple": "805b87", + "mid green": "50a747", + "greyish": "a8a495", + "neon yellow": "cfff04", + "banana": "ffff7e", + "carnation pink": "ff7fa7", + "tomato": "ef4026", + "sea": "3c9992", + "muddy brown": "886806", + "turquoise green": "04f489", + "buff": "fef69e", + "fawn": "cfaf7b", + "muted blue": "3b719f", + "pale rose": "fdc1c5", + "dark mint green": "20c073", + "amethyst": "9b5fc0", + "blue/green": "0f9b8e", + "chestnut": "742802", + "sick green": "9db92c", + "pea": "a4bf20", + "rusty orange": "cd5909", + "stone": "ada587", + "rose red": "be013c", + "pale aqua": "b8ffeb", + "deep orange": "dc4d01", + "earth": "a2653e", + "mossy green": "638b27", + "grassy green": "419c03", + "pale lime green": "b1ff65", + "light grey blue": "9dbcd4", + "pale grey": "fdfdfe", + "asparagus": "77ab56", + "blueberry": "464196", + "purple red": "990147", + "pale lime": "befd73", + "greenish teal": "32bf84", + "caramel": "af6f09", + "deep magenta": "a0025c", + "light peach": "ffd8b1", + "milk chocolate": "7f4e1e", + "ocher": "bf9b0c", + "off green": "6ba353", + "purply pink": "f075e6", + "lightblue": "7bc8f6", + "dusky blue": "475f94", + "golden": "f5bf03", + "light beige": "fffeb6", + "butter yellow": "fffd74", + "dusky purple": "895b7b", + "french blue": "436bad", + "ugly yellow": "d0c101", + "greeny yellow": "c6f808", + "orangish red": "f43605", + "shamrock green": "02c14d", + "orangish brown": "b25f03", + "tree green": "2a7e19", + "deep violet": "490648", + "gunmetal": "536267", + "blue/purple": "5a06ef", + "cherry": "cf0234", + "sandy brown": "c4a661", + "warm grey": "978a84", + "dark indigo": "1f0954", + "midnight": "03012d", + "bluey green": "2bb179", + "grey pink": "c3909b", + "soft purple": "a66fb5", + "blood": "770001", + "brown red": "922b05", + "medium grey": "7d7f7c", + "berry": "990f4b", + "poo": "8f7303", + "purpley pink": "c83cb9", + "light salmon": "fea993", + "snot": "acbb0d", + "easter purple": "c071fe", + "light yellow green": "ccfd7f", + "dark navy blue": "00022e", + "drab": "828344", + "light rose": "ffc5cb", + "rouge": "ab1239", + "purplish red": "b0054b", + "slime green": "99cc04", + "baby poop": "937c00", + "irish green": "019529", + "pink/purple": "ef1de7", + "dark navy": "000435", + "greeny blue": "42b395", + "light plum": "9d5783", + "pinkish grey": "c8aca9", + "dirty orange": "c87606", + "rust red": "aa2704", + "pale lilac": "e4cbff", + "orangey red": "fa4224", + "primary blue": "0804f9", + "kermit green": "5cb200", + "brownish purple": "76424e", + "murky green": "6c7a0e", + "wheat": "fbdd7e", + "very dark purple": "2a0134", + "bottle green": "044a05", + "watermelon": "fd4659", + "deep sky blue": "0d75f8", + "fire engine red": "fe0002", + "yellow ochre": "cb9d06", + "pumpkin orange": "fb7d07", + "pale olive": "b9cc81", + "light lilac": "edc8ff", + "lightish green": "61e160", + "carolina blue": "8ab8fe", + "mulberry": "920a4e", + "shocking pink": "fe02a2", + "auburn": "9a3001", + "bright lime green": "65fe08", + "celadon": "befdb7", + "pinkish brown": "b17261", + "poo brown": "885f01", + "bright sky blue": "02ccfe", + "celery": "c1fd95", + "dirt brown": "836539", + "strawberry": "fb2943", + "dark lime": "84b701", + "copper": "b66325", + "medium brown": "7f5112", + "muted green": "5fa052", + "robin's egg": "6dedfd", + "bright aqua": "0bf9ea", + "bright lavender": "c760ff", + "ivory": "ffffcb", + "very light purple": "f6cefc", + "light navy": "155084", + "pink red": "f5054f", + "olive brown": "645403", + "poop brown": "7a5901", + "mustard green": "a8b504", + "ocean green": "3d9973", + "very dark blue": "000133", + "dusty green": "76a973", + "light navy blue": "2e5a88", + "minty green": "0bf77d", + "adobe": "bd6c48", + "barney": "ac1db8", + "jade green": "2baf6a", + "bright light blue": "26f7fd", + "light lime": "aefd6c", + "dark khaki": "9b8f55", + "orange yellow": "ffad01", + "ocre": "c69c04", + "maize": "f4d054", + "faded pink": "de9dac", + "british racing green": "05480d", + "sandstone": "c9ae74", + "mud brown": "60460f", + "light sea green": "98f6b0", + "robin egg blue": "8af1fe", + "aqua marine": "2ee8bb", + "dark sea green": "11875d", + "soft pink": "fdb0c0", + "orangey brown": "b16002", + "cherry red": "f7022a", + "burnt yellow": "d5ab09", + "brownish grey": "86775f", + "camel": "c69f59", + "purplish grey": "7a687f", + "marine": "042e60", + "greyish pink": "c88d94", + "pale turquoise": "a5fbd5", + "pastel yellow": "fffe71", + "bluey purple": "6241c7", + "canary yellow": "fffe40", + "faded red": "d3494e", + "sepia": "985e2b", + "coffee": "a6814c", + "bright magenta": "ff08e8", + "mocha": "9d7651", + "ecru": "feffca", + "purpleish": "98568d", + "cranberry": "9e003a", + "darkish green": "287c37", + "brown orange": "b96902", + "dusky rose": "ba6873", + "melon": "ff7855", + "sickly green": "94b21c", + "silver": "c5c9c7", + "purply blue": "661aee", + "purpleish blue": "6140ef", + "hospital green": "9be5aa", + "shit brown": "7b5804", + "mid blue": "276ab3", + "amber": "feb308", + "easter green": "8cfd7e", + "soft blue": "6488ea", + "cerulean blue": "056eee", + "golden brown": "b27a01", + "bright turquoise": "0ffef9", + "red pink": "fa2a55", + "red purple": "820747", + "greyish brown": "7a6a4f", + "vermillion": "f4320c", + "russet": "a13905", + "steel grey": "6f828a", + "lighter purple": "a55af4", + "bright violet": "ad0afd", + "prussian blue": "004577", + "slate green": "658d6d", + "dirty pink": "ca7b80", + "dark blue green": "005249", + "pine": "2b5d34", + "yellowy green": "bff128", + "dark gold": "b59410", + "bluish": "2976bb", + "darkish blue": "014182", + "dull red": "bb3f3f", + "pinky red": "fc2647", + "bronze": "a87900", + "pale teal": "82cbb2", + "military green": "667c3e", + "barbie pink": "fe46a5", + "bubblegum pink": "fe83cc", + "pea soup green": "94a617", + "dark mustard": "a88905", + "shit": "7f5f00", + "medium purple": "9e43a2", + "very dark green": "062e03", + "dirt": "8a6e45", + "dusky pink": "cc7a8b", + "red violet": "9e0168", + "lemon yellow": "fdff38", + "pistachio": "c0fa8b", + "dull yellow": "eedc5b", + "dark lime green": "7ebd01", + "denim blue": "3b5b92", + "teal blue": "01889f", + "lightish blue": "3d7afd", + "purpley blue": "5f34e7", + "light indigo": "6d5acf", + "swamp green": "748500", + "brown green": "706c11", + "dark maroon": "3c0008", + "hot purple": "cb00f5", + "dark forest green": "002d04", + "faded blue": "658cbb", + "drab green": "749551", + "light lime green": "b9ff66", + "snot green": "9dc100", + "yellowish": "faee66", + "light blue green": "7efbb3", + "bordeaux": "7b002c", + "light mauve": "c292a1", + "ocean": "017b92", + "marigold": "fcc006", + "muddy green": "657432", + "dull orange": "d8863b", + "steel": "738595", + "electric purple": "aa23ff", + "fluorescent green": "08ff08", + "yellowish brown": "9b7a01", + "blush": "f29e8e", + "soft green": "6fc276", + "bright orange": "ff5b00", + "lemon": "fdff52", + "purple grey": "866f85", + "acid green": "8ffe09", + "pale lavender": "eecffe", + "violet blue": "510ac9", + "light forest green": "4f9153", + "burnt red": "9f2305", + "khaki green": "728639", + "cerise": "de0c62", + "faded purple": "916e99", + "apricot": "ffb16d", + "dark olive green": "3c4d03", + "grey brown": "7f7053", + "green grey": "77926f", + "true blue": "010fcc", + "pale violet": "ceaefa", + "periwinkle blue": "8f99fb", + "light sky blue": "c6fcff", + "blurple": "5539cc", + "green brown": "544e03", + "bluegreen": "017a79", + "bright teal": "01f9c6", + "brownish yellow": "c9b003", + "pea soup": "929901", + "forest": "0b5509", + "barney purple": "a00498", + "ultramarine": "2000b1", + "purplish": "94568c", + "puke yellow": "c2be0e", + "bluish grey": "748b97", + "dark periwinkle": "665fd1", + "dark lilac": "9c6da5", + "reddish": "c44240", + "light maroon": "a24857", + "dusty purple": "825f87", + "terra cotta": "c9643b", + "avocado": "90b134", + "marine blue": "01386a", + "teal green": "25a36f", + "slate grey": "59656d", + "lighter green": "75fd63", + "electric green": "21fc0d", + "dusty blue": "5a86ad", + "golden yellow": "fec615", + "bright yellow": "fffd01", + "light lavender": "dfc5fe", + "umber": "b26400", + "poop": "7f5e00", + "dark peach": "de7e5d", + "jungle green": "048243", + "eggshell": "ffffd4", + "denim": "3b638c", + "yellow brown": "b79400", + "dull purple": "84597e", + "chocolate brown": "411900", + "wine red": "7b0323", + "neon blue": "04d9ff", + "dirty green": "667e2c", + "light tan": "fbeeac", + "ice blue": "d7fffe", + "cadet blue": "4e7496", + "dark mauve": "874c62", + "very light blue": "d5ffff", + "grey purple": "826d8c", + "pastel pink": "ffbacd", + "very light green": "d1ffbd", + "dark sky blue": "448ee4", + "evergreen": "05472a", + "dull pink": "d5869d", + "aubergine": "3d0734", + "mahogany": "4a0100", + "reddish orange": "f8481c", + "deep green": "02590f", + "vomit green": "89a203", + "purple pink": "e03fd8", + "dusty pink": "d58a94", + "faded green": "7bb274", + "camo green": "526525", + "pinky purple": "c94cbe", + "pink purple": "db4bda", + "brownish red": "9e3623", + "dark rose": "b5485d", + "mud": "735c12", + "brownish": "9c6d57", + "emerald green": "028f1e", + "pale brown": "b1916e", + "dull blue": "49759c", + "burnt umber": "a0450e", + "medium green": "39ad48", + "clay": "b66a50", + "light aqua": "8cffdb", + "light olive green": "a4be5c", + "brownish orange": "cb7723", + "dark aqua": "05696b", + "purplish pink": "ce5dae", + "dark salmon": "c85a53", + "greenish grey": "96ae8d", + "jade": "1fa774", + "ugly green": "7a9703", + "dark beige": "ac9362", + "emerald": "01a049", + "pale red": "d9544d", + "light magenta": "fa5ff7", + "sky": "82cafc", + "light cyan": "acfffc", + "yellow orange": "fcb001", + "reddish purple": "910951", + "reddish pink": "fe2c54", + "orchid": "c875c4", + "dirty yellow": "cdc50a", + "orange red": "fd411e", + "deep red": "9a0200", + "orange brown": "be6400", + "cobalt blue": "030aa7", + "neon pink": "fe019a", + "rose pink": "f7879a", + "greyish purple": "887191", + "raspberry": "b00149", + "aqua green": "12e193", + "salmon pink": "fe7b7c", + "tangerine": "ff9408", + "brownish green": "6a6e09", + "red brown": "8b2e16", + "greenish brown": "696112", + "pumpkin": "e17701", + "pine green": "0a481e", + "charcoal": "343837", + "baby pink": "ffb7ce", + "cornflower": "6a79f7", + "blue violet": "5d06e9", + "chocolate": "3d1c02", + "greyish green": "82a67d", + "scarlet": "be0119", + "green yellow": "c9ff27", + "dark olive": "373e02", + "sienna": "a9561e", + "pastel purple": "caa0ff", + "terracotta": "ca6641", + "aqua blue": "02d8e9", + "sage green": "88b378", + "blood red": "980002", + "deep pink": "cb0162", + "grass": "5cac2d", + "moss": "769958", + "pastel blue": "a2bffe", + "bluish green": "10a674", + "green blue": "06b48b", + "dark tan": "af884a", + "greenish blue": "0b8b87", + "pale orange": "ffa756", + "vomit": "a2a415", + "forrest green": "154406", + "dark lavender": "856798", + "dark violet": "34013f", + "purple blue": "632de9", + "dark cyan": "0a888a", + "olive drab": "6f7632", + "pinkish": "d46a7e", + "cobalt": "1e488f", + "neon purple": "bc13fe", + "light turquoise": "7ef4cc", + "apple green": "76cd26", + "dull green": "74a662", + "wine": "80013f", + "powder blue": "b1d1fc", + "off white": "ffffe4", + "electric blue": "0652ff", + "dark turquoise": "045c5a", + "blue purple": "5729ce", + "azure": "069af3", + "bright red": "ff000d", + "pinkish red": "f10c45", + "cornflower blue": "5170d7", + "light olive": "acbf69", + "grape": "6c3461", + "greyish blue": "5e819d", + "purplish blue": "601ef9", + "yellowish green": "b0dd16", + "greenish yellow": "cdfd02", + "medium blue": "2c6fbb", + "dusty rose": "c0737a", + "light violet": "d6b4fc", + "midnight blue": "020035", + "bluish purple": "703be7", + "red orange": "fd3c06", + "dark magenta": "960056", + "greenish": "40a368", + "ocean blue": "03719c", + "coral": "fc5a50", + "cream": "ffffc2", + "reddish brown": "7f2b0a", + "burnt sienna": "b04e0f", + "brick": "a03623", + "sage": "87ae73", + "grey green": "789b73", + "white": "ffffff", + "robin's egg blue": "98eff9", + "moss green": "658b38", + "steel blue": "5a7d9a", + "eggplant": "380835", + "light yellow": "fffe7a", + "leaf green": "5ca904", + "light grey": "d8dcd6", + "puke": "a5a502", + "pinkish purple": "d648d7", + "sea blue": "047495", + "pale purple": "b790d4", + "slate blue": "5b7c99", + "blue grey": "607c8e", + "hunter green": "0b4008", + "fuchsia": "ed0dd9", + "crimson": "8c000f", + "pale yellow": "ffff84", + "ochre": "bf9005", + "mustard yellow": "d2bd0a", + "light red": "ff474c", + "cerulean": "0485d1", + "pale pink": "ffcfdc", + "deep blue": "040273", + "rust": "a83c09", + "light teal": "90e4c1", + "slate": "516572", + "goldenrod": "fac205", + "dark yellow": "d5b60a", + "dark grey": "363737", + "army green": "4b5d16", + "grey blue": "6b8ba4", + "seafoam": "80f9ad", + "puce": "a57e52", + "spring green": "a9f971", + "dark orange": "c65102", + "sand": "e2ca76", + "pastel green": "b0ff9d", + "mint": "9ffeb0", + "light orange": "fdaa48", + "bright pink": "fe01b1", + "chartreuse": "c1f80a", + "deep purple": "36013f", + "dark brown": "341c02", + "taupe": "b9a281", + "pea green": "8eab12", + "puke green": "9aae07", + "kelly green": "02ab2e", + "seafoam green": "7af9ab", + "blue green": "137e6d", + "khaki": "aaa662", + "burgundy": "610023", + "dark teal": "014d4e", + "brick red": "8f1402", + "royal purple": "4b006e", + "plum": "580f41", + "mint green": "8fff9f", + "gold": "dbb40c", + "baby blue": "a2cffe", + "yellow green": "c0fb2d", + "bright purple": "be03fd", + "dark red": "840000", + "pale blue": "d0fefe", + "grass green": "3f9b0b", + "navy": "01153e", + "aquamarine": "04d8b2", + "burnt orange": "c04e01", + "neon green": "0cff0c", + "bright blue": "0165fc", + "rose": "cf6275", + "light pink": "ffd1df", + "mustard": "ceb301", + "indigo": "380282", + "lime": "aaff32", + "sea green": "53fca1", + "periwinkle": "8e82fe", + "dark pink": "cb416b", + "olive green": "677a04", + "peach": "ffb07c", + "pale green": "c7fdb5", + "light brown": "ad8150", + "hot pink": "ff028d", + "black": "000000", + "lilac": "cea2fd", + "navy blue": "001146", + "royal blue": "0504aa", + "beige": "e6daa6", + "salmon": "ff796c", + "olive": "6e750e", + "maroon": "650021", + "bright green": "01ff07", + "dark purple": "35063e", + "mauve": "ae7181", + "forest green": "06470c", + "aqua": "13eac9", + "cyan": "00ffff", + "tan": "d1b26f", + "dark blue": "00035b", + "lavender": "c79fef", + "turquoise": "06c2ac", + "dark green": "033500", + "violet": "9a0eea", + "light purple": "bf77f6", + "lime green": "89fe05", + "grey": "929591", + "sky blue": "75bbfd", + "yellow": "ffff14", + "magenta": "c20078", + "light green": "96f97b", + "orange": "f97306", + "teal": "029386", + "light blue": "95d0fc", + "red": "e50000", + "brown": "653700", + "pink": "ff81c0", + "blue": "0343df", + "green": "15b01a", + "purple": "7e1e9c", } # Normalize name to "xkcd:" to avoid name collisions. @@ -1029,154 +1029,154 @@ # https://drafts.csswg.org/css-color-4/#named-colors CSS4_COLORS = { - "aliceblue": "#F0F8FF", - "antiquewhite": "#FAEBD7", - "aqua": "#00FFFF", - "aquamarine": "#7FFFD4", - "azure": "#F0FFFF", - "beige": "#F5F5DC", - "bisque": "#FFE4C4", - "black": "#000000", - "blanchedalmond": "#FFEBCD", - "blue": "#0000FF", - "blueviolet": "#8A2BE2", - "brown": "#A52A2A", - "burlywood": "#DEB887", - "cadetblue": "#5F9EA0", - "chartreuse": "#7FFF00", - "chocolate": "#D2691E", - "coral": "#FF7F50", - "cornflowerblue": "#6495ED", - "cornsilk": "#FFF8DC", - "crimson": "#DC143C", - "cyan": "#00FFFF", - "darkblue": "#00008B", - "darkcyan": "#008B8B", - "darkgoldenrod": "#B8860B", - "darkgray": "#A9A9A9", - "darkgreen": "#006400", - "darkgrey": "#A9A9A9", - "darkkhaki": "#BDB76B", - "darkmagenta": "#8B008B", - "darkolivegreen": "#556B2F", - "darkorange": "#FF8C00", - "darkorchid": "#9932CC", - "darkred": "#8B0000", - "darksalmon": "#E9967A", - "darkseagreen": "#8FBC8F", - "darkslateblue": "#483D8B", - "darkslategray": "#2F4F4F", - "darkslategrey": "#2F4F4F", - "darkturquoise": "#00CED1", - "darkviolet": "#9400D3", - "deeppink": "#FF1493", - "deepskyblue": "#00BFFF", - "dimgray": "#696969", - "dimgrey": "#696969", - "dodgerblue": "#1E90FF", - "firebrick": "#B22222", - "floralwhite": "#FFFAF0", - "forestgreen": "#228B22", - "fuchsia": "#FF00FF", - "gainsboro": "#DCDCDC", - "ghostwhite": "#F8F8FF", - "gold": "#FFD700", - "goldenrod": "#DAA520", - "gray": "#808080", - "green": "#008000", - "greenyellow": "#ADFF2F", - "grey": "#808080", - "honeydew": "#F0FFF0", - "hotpink": "#FF69B4", - "indianred": "#CD5C5C", - "indigo": "#4B0082", - "ivory": "#FFFFF0", - "khaki": "#F0E68C", - "lavender": "#E6E6FA", - "lavenderblush": "#FFF0F5", - "lawngreen": "#7CFC00", - "lemonchiffon": "#FFFACD", - "lightblue": "#ADD8E6", - "lightcoral": "#F08080", - "lightcyan": "#E0FFFF", - "lightgoldenrodyellow": "#FAFAD2", - "lightgray": "#D3D3D3", - "lightgreen": "#90EE90", - "lightgrey": "#D3D3D3", - "lightpink": "#FFB6C1", - "lightsalmon": "#FFA07A", - "lightseagreen": "#20B2AA", - "lightskyblue": "#87CEFA", - "lightslategray": "#778899", - "lightslategrey": "#778899", - "lightsteelblue": "#B0C4DE", - "lightyellow": "#FFFFE0", - "lime": "#00FF00", - "limegreen": "#32CD32", - "linen": "#FAF0E6", - "magenta": "#FF00FF", - "maroon": "#800000", - "mediumaquamarine": "#66CDAA", - "mediumblue": "#0000CD", - "mediumorchid": "#BA55D3", - "mediumpurple": "#9370DB", - "mediumseagreen": "#3CB371", - "mediumslateblue": "#7B68EE", - "mediumspringgreen": "#00FA9A", - "mediumturquoise": "#48D1CC", - "mediumvioletred": "#C71585", - "midnightblue": "#191970", - "mintcream": "#F5FFFA", - "mistyrose": "#FFE4E1", - "moccasin": "#FFE4B5", - "navajowhite": "#FFDEAD", - "navy": "#000080", - "oldlace": "#FDF5E6", - "olive": "#808000", - "olivedrab": "#6B8E23", - "orange": "#FFA500", - "orangered": "#FF4500", - "orchid": "#DA70D6", - "palegoldenrod": "#EEE8AA", - "palegreen": "#98FB98", - "paleturquoise": "#AFEEEE", - "palevioletred": "#DB7093", - "papayawhip": "#FFEFD5", - "peachpuff": "#FFDAB9", - "peru": "#CD853F", - "pink": "#FFC0CB", - "plum": "#DDA0DD", - "powderblue": "#B0E0E6", - "purple": "#800080", - "rebeccapurple": "#663399", - "red": "#FF0000", - "rosybrown": "#BC8F8F", - "royalblue": "#4169E1", - "saddlebrown": "#8B4513", - "salmon": "#FA8072", - "sandybrown": "#F4A460", - "seagreen": "#2E8B57", - "seashell": "#FFF5EE", - "sienna": "#A0522D", - "silver": "#C0C0C0", - "skyblue": "#87CEEB", - "slateblue": "#6A5ACD", - "slategray": "#708090", - "slategrey": "#708090", - "snow": "#FFFAFA", - "springgreen": "#00FF7F", - "steelblue": "#4682B4", - "tan": "#D2B48C", - "teal": "#008080", - "thistle": "#D8BFD8", - "tomato": "#FF6347", - "turquoise": "#40E0D0", - "violet": "#EE82EE", - "wheat": "#F5DEB3", - "white": "#FFFFFF", - "whitesmoke": "#F5F5F5", - "yellow": "#FFFF00", - "yellowgreen": "#9ACD32", + "aliceblue": "F0F8FF", + "antiquewhite": "FAEBD7", + "aqua": "00FFFF", + "aquamarine": "7FFFD4", + "azure": "F0FFFF", + "beige": "F5F5DC", + "bisque": "FFE4C4", + "black": "000000", + "blanchedalmond": "FFEBCD", + "blue": "0000FF", + "blueviolet": "8A2BE2", + "brown": "A52A2A", + "burlywood": "DEB887", + "cadetblue": "5F9EA0", + "chartreuse": "7FFF00", + "chocolate": "D2691E", + "coral": "FF7F50", + "cornflowerblue": "6495ED", + "cornsilk": "FFF8DC", + "crimson": "DC143C", + "cyan": "00FFFF", + "darkblue": "00008B", + "darkcyan": "008B8B", + "darkgoldenrod": "B8860B", + "darkgray": "A9A9A9", + "darkgreen": "006400", + "darkgrey": "A9A9A9", + "darkkhaki": "BDB76B", + "darkmagenta": "8B008B", + "darkolivegreen": "556B2F", + "darkorange": "FF8C00", + "darkorchid": "9932CC", + "darkred": "8B0000", + "darksalmon": "E9967A", + "darkseagreen": "8FBC8F", + "darkslateblue": "483D8B", + "darkslategray": "2F4F4F", + "darkslategrey": "2F4F4F", + "darkturquoise": "00CED1", + "darkviolet": "9400D3", + "deeppink": "FF1493", + "deepskyblue": "00BFFF", + "dimgray": "696969", + "dimgrey": "696969", + "dodgerblue": "1E90FF", + "firebrick": "B22222", + "floralwhite": "FFFAF0", + "forestgreen": "228B22", + "fuchsia": "FF00FF", + "gainsboro": "DCDCDC", + "ghostwhite": "F8F8FF", + "gold": "FFD700", + "goldenrod": "DAA520", + "gray": "808080", + "green": "008000", + "greenyellow": "ADFF2F", + "grey": "808080", + "honeydew": "F0FFF0", + "hotpink": "FF69B4", + "indianred": "CD5C5C", + "indigo": "4B0082", + "ivory": "FFFFF0", + "khaki": "F0E68C", + "lavender": "E6E6FA", + "lavenderblush": "FFF0F5", + "lawngreen": "7CFC00", + "lemonchiffon": "FFFACD", + "lightblue": "ADD8E6", + "lightcoral": "F08080", + "lightcyan": "E0FFFF", + "lightgoldenrodyellow": "FAFAD2", + "lightgray": "D3D3D3", + "lightgreen": "90EE90", + "lightgrey": "D3D3D3", + "lightpink": "FFB6C1", + "lightsalmon": "FFA07A", + "lightseagreen": "20B2AA", + "lightskyblue": "87CEFA", + "lightslategray": "778899", + "lightslategrey": "778899", + "lightsteelblue": "B0C4DE", + "lightyellow": "FFFFE0", + "lime": "00FF00", + "limegreen": "32CD32", + "linen": "FAF0E6", + "magenta": "FF00FF", + "maroon": "800000", + "mediumaquamarine": "66CDAA", + "mediumblue": "0000CD", + "mediumorchid": "BA55D3", + "mediumpurple": "9370DB", + "mediumseagreen": "3CB371", + "mediumslateblue": "7B68EE", + "mediumspringgreen": "00FA9A", + "mediumturquoise": "48D1CC", + "mediumvioletred": "C71585", + "midnightblue": "191970", + "mintcream": "F5FFFA", + "mistyrose": "FFE4E1", + "moccasin": "FFE4B5", + "navajowhite": "FFDEAD", + "navy": "000080", + "oldlace": "FDF5E6", + "olive": "808000", + "olivedrab": "6B8E23", + "orange": "FFA500", + "orangered": "FF4500", + "orchid": "DA70D6", + "palegoldenrod": "EEE8AA", + "palegreen": "98FB98", + "paleturquoise": "AFEEEE", + "palevioletred": "DB7093", + "papayawhip": "FFEFD5", + "peachpuff": "FFDAB9", + "peru": "CD853F", + "pink": "FFC0CB", + "plum": "DDA0DD", + "powderblue": "B0E0E6", + "purple": "800080", + "rebeccapurple": "663399", + "red": "FF0000", + "rosybrown": "BC8F8F", + "royalblue": "4169E1", + "saddlebrown": "8B4513", + "salmon": "FA8072", + "sandybrown": "F4A460", + "seagreen": "2E8B57", + "seashell": "FFF5EE", + "sienna": "A0522D", + "silver": "C0C0C0", + "skyblue": "87CEEB", + "slateblue": "6A5ACD", + "slategray": "708090", + "slategrey": "708090", + "snow": "FFFAFA", + "springgreen": "00FF7F", + "steelblue": "4682B4", + "tan": "D2B48C", + "teal": "008080", + "thistle": "D8BFD8", + "tomato": "FF6347", + "turquoise": "40E0D0", + "violet": "EE82EE", + "wheat": "F5DEB3", + "white": "FFFFFF", + "whitesmoke": "F5F5F5", + "yellow": "FFFF00", + "yellowgreen": "9ACD32", } # Normalize name to "tab:" to avoid name collisions. diff --git a/core/changelog.py b/core/changelog.py index 3bf4ad865b..f687cfa57a 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -46,11 +46,12 @@ class Version: ACTION_REGEX = r"###\s*(.+?)\s*\n(.*?)(?=###\s*.+?|$)" DESCRIPTION_REGEX = r"^(.*?)(?=###\s*.+?|$)" - def __init__(self, bot, version: str, lines: str): + def __init__(self, bot, branch: str, version: str, lines: str): self.bot = bot self.version = version.lstrip("vV") self.lines = lines.strip() self.fields = {} + self.changelog_url = f"https://github.com/kyb3r/modmail/blob/{branch}/CHANGELOG.md" self.description = "" self.parse() @@ -79,7 +80,7 @@ def parse(self) -> None: @property def url(self) -> str: - return f"{Changelog.CHANGELOG_URL}#v{self.version[::2]}" + return f"{self.changelog_url}#v{self.version[::2]}" @property def embed(self) -> Embed: @@ -122,28 +123,19 @@ class Changelog: Class Attributes ---------------- - RAW_CHANGELOG_URL : str - The URL to Modmail changelog. - CHANGELOG_URL : str - The URL to Modmail changelog directly from in GitHub. VERSION_REGEX : re.Pattern The regex used to parse the versions. """ - - RAW_CHANGELOG_URL = ( - "https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md" - ) - CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md" VERSION_REGEX = re.compile( r"#\s*([vV]\d+\.\d+(?:\.\d+)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)?|$)", flags=re.DOTALL, ) - def __init__(self, bot, text: str): + def __init__(self, bot, branch: str, text: str): self.bot = bot self.text = text logger.debug("Fetching changelog from GitHub.") - self.versions = [Version(bot, *m) for m in self.VERSION_REGEX.findall(text)] + self.versions = [Version(bot, branch, *m) for m in self.VERSION_REGEX.findall(text)] @property def latest_version(self) -> Version: @@ -169,7 +161,6 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": bot : Bot The Modmail bot. url : str, optional - Defaults to `RAW_CHANGELOG_URL`. The URL to the changelog. Returns @@ -177,6 +168,8 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": Changelog The newly created `Changelog` parsed from the `url`. """ - url = url or cls.RAW_CHANGELOG_URL - resp = await bot.session.get(url) - return cls(bot, await resp.text()) + branch = 'master' if not bot.version.is_prerelease else 'development' + url = url or f"https://raw.githubusercontent.com/kyb3r/modmail/{branch}/CHANGELOG.md" + + async with await bot.session.get(url) as resp: + return cls(bot, branch, await resp.text()) diff --git a/core/config.py b/core/config.py index ed9adead23..f20049e10a 100644 --- a/core/config.py +++ b/core/config.py @@ -100,6 +100,7 @@ class ConfigManager: "token": None, # Logging "log_level": "INFO", + "load_plugins": True, } colors = {"mod_color", "recipient_color", "main_color"} @@ -113,6 +114,7 @@ class ConfigManager: "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", + "load_plugins" } defaults = {**public_keys, **private_keys, **protected_keys} @@ -163,15 +165,83 @@ def populate_cache(self) -> dict: return self._cache - async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: - value_text = val - clean_value = val + async def update(self): + """Updates the config with data from the cache""" + await self.bot.api.update_config(self.filter_default(self._cache)) + + async def refresh(self) -> dict: + """Refreshes internal cache with data from database""" + for k, v in (await self.bot.api.get_config()).items(): + k = k.lower() + if k in self.all_keys: + self._cache[k] = v + if not self.ready_event.is_set(): + self.ready_event.set() + logger.info("Successfully fetched configurations from database.") + return self._cache + + async def wait_until_ready(self) -> None: + await self.ready_event.wait() + + def __setitem__(self, key: str, item: typing.Any) -> None: + key = key.lower() + logger.info("Setting %s.", key) + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + self._cache[key] = item + + def __getitem__(self, key: str) -> typing.Any: + key = key.lower() + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + if key not in self._cache: + self._cache[key] = deepcopy(self.defaults[key]) + return self._cache[key] + + def __delitem__(self, key: str) -> None: + return self.remove(key) + + def get(self, key: str, convert=True) -> typing.Any: + value = self.__getitem__(key) + + if not convert: + return value - # when setting a color if key in self.colors: try: - hex_ = str(val) + return int(value.lstrip("#"), base=16) + except ValueError: + logger.error("Invalid %s provided.", key) + value = int(self.remove(key).lstrip("#"), base=16) + + elif key in self.time_deltas: + if value is None: + return + try: + value = isodate.parse_duration(value) + except isodate.ISO8601Error: + logger.warning( + "The {account} age limit needs to be a " + 'ISO-8601 duration formatted duration, not "%s".', + value, + ) + value = self.remove(key) + + elif key in self.booleans: + try: + value = strtobool(value) + except ValueError: + value = self.remove(key) + + return value + + def set(self, key: str, item: typing.Any, convert=True) -> None: + if not convert: + return self.__setitem__(key, item) + if key in self.colors: + try: + hex_ = str(item) if hex_.startswith("#"): hex_ = hex_[1:] if len(hex_) == 3: @@ -182,11 +252,9 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: int(hex_, 16) except ValueError: raise InvalidConfigError("Invalid color name or hex.") - hex_ = "#" + hex_ - value_text = clean_value = hex_ except InvalidConfigError: - name = str(val).lower() + name = str(item).lower() name = re.sub(r"[\-+|. ]+", " ", name) hex_ = ALL_COLORS.get(name) if hex_ is None: @@ -194,17 +262,15 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: hex_ = ALL_COLORS.get(name) if hex_ is None: raise + return self.__setitem__(key, '#' + hex_) - clean_value = hex_ - value_text = f"{name} ({clean_value})" - - elif key in self.time_deltas: + if key in self.time_deltas: try: - isodate.parse_duration(val) + isodate.parse_duration(item) except isodate.ISO8601Error: try: converter = UserFriendlyTime() - time = await converter.convert(None, val) + time = self.bot.loop.run_until_complete(converter.convert(None, item)) if time.arg: raise ValueError except BadArgument as exc: @@ -214,73 +280,24 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: "Unrecognized time, please use ISO-8601 duration format " 'string or a simpler "human readable" time.' ) - clean_value = isodate.duration_isoformat(time.dt - converter.now) - value_text = f"{val} ({clean_value})" + item = isodate.duration_isoformat(time.dt - converter.now) + return self.__setitem__(key, item) - elif key in self.booleans: + if key in self.booleans: try: - clean_value = value_text = strtobool(val) + return self.__setitem__(key, strtobool(item)) except ValueError: raise InvalidConfigError("Must be a yes/no value.") - return clean_value, value_text - - async def update(self): - """Updates the config with data from the cache""" - await self.bot.api.update_config(self.filter_default(self._cache)) - - async def refresh(self) -> dict: - """Refreshes internal cache with data from database""" - for k, v in (await self.bot.api.get_config()).items(): - k = k.lower() - if k in self.all_keys: - self._cache[k] = v - if not self.ready_event.is_set(): - self.ready_event.set() - logger.info("Successfully fetched configurations from database.") - return self._cache - - async def wait_until_ready(self) -> None: - await self.ready_event.wait() - - def __setitem__(self, key: str, item: typing.Any) -> None: - key = key.lower() - logger.info("Setting %s.", key) - if key not in self.all_keys: - raise InvalidConfigError(f'Configuration "{key}" is invalid.') - self._cache[key] = item - - def __getitem__(self, key: str) -> typing.Any: - key = key.lower() - if key not in self.all_keys: - raise InvalidConfigError(f'Configuration "{key}" is invalid.') - if key not in self._cache: - self._cache[key] = deepcopy(self.defaults[key]) - return self._cache[key] - - def get(self, key: str, default: typing.Any = Default) -> typing.Any: - key = key.lower() - if key not in self.all_keys: - raise InvalidConfigError(f'Configuration "{key}" is invalid.') - if key not in self._cache: - if default is Default: - self._cache[key] = deepcopy(self.defaults[key]) - if default is not Default and self._cache[key] == self.defaults[key]: - self._cache[key] = default - return self._cache[key] - - def set(self, key: str, item: typing.Any) -> None: - key = key.lower() - logger.info("Setting %s.", key) - if key not in self.all_keys: - raise InvalidConfigError(f'Configuration "{key}" is invalid.') - self._cache[key] = item + return self.__setitem__(key, item) def remove(self, key: str) -> typing.Any: key = key.lower() logger.info("Removing %s.", key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') + if key in self._cache: + del self._cache[key] self._cache[key] = deepcopy(self.defaults[key]) return self._cache[key] diff --git a/core/config_help.json b/core/config_help.json index 50d0a9fad9..3b5028ab28 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -467,5 +467,14 @@ "notes": [ "This configuration can only to be set through `.env` file or environment (config) variables." ] + }, + "load_plugins": { + "default": "Yes", + "description": "Whether plugins should be loaded into Modmail.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] } } diff --git a/core/thread.py b/core/thread.py index bdf0bc7aa2..9686e5ecb1 100644 --- a/core/thread.py +++ b/core/thread.py @@ -12,7 +12,7 @@ from discord.ext.commands import MissingRequiredArgument, CommandError from core.time import human_timedelta -from core.utils import is_image_url, days, match_user_id, truncate, ignore, strtobool +from core.utils import is_image_url, days, match_user_id, truncate, ignore logger = logging.getLogger("Modmail") @@ -161,12 +161,7 @@ async def send_genesis_message(): timestamp=channel.created_at, ) - try: - recipient_thread_close = strtobool( - self.bot.config["recipient_thread_close"] - ) - except ValueError: - recipient_thread_close = self.bot.config.remove("recipient_thread_close") + recipient_thread_close = self.bot.config.get("recipient_thread_close") if recipient_thread_close: footer = self.bot.config["thread_self_closable_creation_footer"] @@ -444,20 +439,7 @@ async def _fetch_timeout( :returns: None if no timeout is set. """ - timeout = self.bot.config["thread_auto_close"] - if timeout: - try: - timeout = isodate.parse_duration(timeout) - except isodate.ISO8601Error: - logger.warning( - "The auto_close_thread limit needs to be a " - "ISO-8601 duration formatted duration string " - 'greater than 0 days, not "%s".', - str(timeout), - ) - timeout = self.bot.config.remove("thread_auto_close") - await self.bot.config.update() - + timeout = self.bot.config.get("thread_auto_close") return timeout async def _restart_close_timer(self): @@ -477,16 +459,7 @@ async def _restart_close_timer(self): reset_time = datetime.utcnow() + timedelta(seconds=seconds) human_time = human_timedelta(dt=reset_time) - try: - thread_auto_close_silently = strtobool( - self.bot.config["thread_auto_close_silently"] - ) - except ValueError: - thread_auto_close_silently = self.bot.config.remove( - "thread_auto_close_silently" - ) - - if thread_auto_close_silently: + if self.bot.config.get('thread_auto_close_silently'): return await self.close( closer=self.bot.user, silent=True, after=int(seconds), auto_close=True ) diff --git a/plugins/registry.json b/plugins/registry.json index fc1dd11a2e..a7d699fa5a 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -187,5 +187,5 @@ "title": "Moderate your server", "icon_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png" - } + } } From 08ab537c13a61200c5751665d983833f51886538 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 20 Sep 2019 01:08:42 -0700 Subject: [PATCH 05/48] v3.3.0-dev1 --- CHANGELOG.md | 9 +- bot.py | 18 ++-- cogs/modmail.py | 53 +++++----- cogs/plugins.py | 105 +++++++++++-------- cogs/utility.py | 230 +++++++++++++++++++++--------------------- core/config.py | 7 +- core/config_help.json | 25 ++++- core/models.py | 5 +- core/thread.py | 20 ++-- core/utils.py | 1 + 10 files changed, 262 insertions(+), 211 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 738f7a3927..dde8659b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,13 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# [UNRELEASED] +# v3.3.0-dev1 + +### Added + +- Two new config vars: + - `ENABLE_PLUGINS` (yes/no default yes), when set to no, plugins will not be loaded into the bot. + - `ERROR_COLOR` (color format, defaults discord red), the color of error messages. ### Changed @@ -19,6 +25,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Logging / plugin-related messages changes. - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). - Help command is in alphabetical order grouped by permissions. +- Notes are no longer always blurple, its set to `MAIN_COLOR` now. ### Internal diff --git a/bot.py b/bot.py index 9108ddf583..f4ff2c1ec4 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev" +__version__ = "3.3.0-dev1" import asyncio import logging @@ -376,6 +376,10 @@ def recipient_color(self) -> int: def main_color(self) -> int: return self.config.get("main_color") + @property + def error_color(self) -> int: + return self.config.get("error_color") + def command_perm(self, command_name: str) -> PermissionLevel: level = self.config["override_command_level"].get(command_name) if level is not None: @@ -608,7 +612,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: title="Message not sent!", description=f"Your must wait for {delta} " f"before you can contact me.", - color=discord.Color.red(), + color=self.error_color, ) ) @@ -632,7 +636,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: title="Message not sent!", description=f"Your must wait for {delta} " f"before you can contact me.", - color=discord.Color.red(), + color=self.error_color, ) ) @@ -950,7 +954,7 @@ async def on_member_remove(self, member): if thread: embed = discord.Embed( description="The recipient has left the server.", - color=discord.Color.red(), + color=self.error_color, ) await thread.channel.send(embed=embed) @@ -1008,14 +1012,14 @@ async def on_command_error(self, context, exception): ) await context.trigger_typing() await context.send( - embed=discord.Embed(color=discord.Color.red(), description=msg) + embed=discord.Embed(color=self.error_color, description=msg) ) elif isinstance(exception, commands.BadArgument): await context.trigger_typing() await context.send( embed=discord.Embed( - color=discord.Color.red(), description=str(exception) + color=self.error_color, description=str(exception) ) ) elif isinstance(exception, commands.CommandNotFound): @@ -1028,7 +1032,7 @@ async def on_command_error(self, context, exception): if hasattr(check, "fail_msg"): await context.send( embed=discord.Embed( - color=discord.Color.red(), description=check.fail_msg + color=self.error_color, description=check.fail_msg ) ) if hasattr(check, "permission_level"): diff --git a/cogs/modmail.py b/cogs/modmail.py index f51c131b67..33d7f28457 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -52,7 +52,7 @@ async def setup(self, ctx): embed = discord.Embed( title="Error", description="Modmail functioning guild not found.", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -165,7 +165,7 @@ async def snippet(self, ctx, *, name: str.lower = None): if not self.bot.snippets: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="You dont have any snippets at the moment.", ) embed.set_footer( @@ -212,7 +212,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte if name in self.bot.snippets: embed = discord.Embed( title="Error", - color=discord.Color.red(), + color=self.bot.error_color, description=f"Snippet `{name}` already exists.", ) return await ctx.send(embed=embed) @@ -220,7 +220,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte if name in self.bot.aliases: embed = discord.Embed( title="Error", - color=discord.Color.red(), + color=self.bot.error_color, description=f"An alias with the same name already exists: `{name}`.", ) return await ctx.send(embed=embed) @@ -228,7 +228,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte if len(name) > 120: embed = discord.Embed( title="Error", - color=discord.Color.red(), + color=self.bot.error_color, description=f"Snippet names cannot be longer than 120 characters.", ) return await ctx.send(embed=embed) @@ -316,8 +316,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = except (discord.HTTPException, discord.InvalidArgument): pass - @staticmethod - async def send_scheduled_close_message(ctx, after, silent=False): + async def send_scheduled_close_message(self, ctx, after, silent=False): human_delta = human_timedelta(after.dt) silent = "*silently* " if silent else "" @@ -325,7 +324,7 @@ async def send_scheduled_close_message(ctx, after, silent=False): embed = discord.Embed( title="Scheduled close", description=f"This thread will close {silent}in {human_delta}.", - color=discord.Color.red(), + color=self.bot.error_color, ) if after.arg and not silent: @@ -375,12 +374,12 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): if thread.close_task is not None or thread.auto_close_task is not None: await thread.cancel_closure(all=True) embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Scheduled close has been cancelled.", ) else: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="This thread has not already been scheduled to close.", ) @@ -432,7 +431,7 @@ async def notify( if mention in mentions: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} is already going to be mentioned.", ) else: @@ -471,7 +470,7 @@ async def unnotify( if mention not in mentions: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} does not have a pending notification.", ) else: @@ -511,7 +510,7 @@ async def subscribe( if mention in mentions: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} is already " "subscribed to this thread.", ) else: @@ -550,7 +549,7 @@ async def unsubscribe( if mention not in mentions: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} is not already " "subscribed to this thread.", ) else: @@ -659,7 +658,7 @@ async def logs(self, ctx, *, user: User = None): if not any(not log["open"] for log in logs): embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="This user does not " "have any previous logs.", ) return await ctx.send(embed=embed) @@ -696,7 +695,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="No log entries have been found for that query", ) return await ctx.send(embed=embed) @@ -729,7 +728,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): if not embeds: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="No log entries have been found for that query.", ) return await ctx.send(embed=embed) @@ -788,11 +787,7 @@ async def find_linked_message(self, ctx, message_id): async for msg in ctx.channel.history(): if message_id is None and msg.embeds: embed = msg.embeds[0] - if isinstance(self.bot.mod_color, discord.Color): - mod_color = self.bot.mod_color.value - else: - mod_color = self.bot.mod_color - if embed.color.value != mod_color or not embed.author.url: + if embed.color.value != self.bot.mod_color or not embed.author.url: continue # TODO: use regex to find the linked message id linked_message_id = str(embed.author.url).split("/")[-1] @@ -823,7 +818,7 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): embed=discord.Embed( title="Failed", description="Cannot find a message to edit.", - color=discord.Color.red(), + color=self.bot.error_color, ) ) @@ -859,7 +854,7 @@ async def contact( if user.bot: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Cannot start a thread with a bot.", ) return await ctx.send(embed=embed) @@ -867,7 +862,7 @@ async def contact( exists = await self.bot.threads.find(recipient=user) if exists: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="A thread for this user already " f"exists in {exists.channel.mention}.", ) @@ -1031,7 +1026,7 @@ async def block( embed = discord.Embed( title="Error", description=f"Cannot block {mention}, user is whitelisted.", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1079,7 +1074,7 @@ async def block( else: embed = discord.Embed( title="Error", - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} is already blocked.", ) @@ -1136,7 +1131,7 @@ async def unblock(self, ctx, *, user: User = None): embed = discord.Embed( title="Error", description=f"{mention} is not blocked.", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1168,7 +1163,7 @@ async def delete(self, ctx, message_id: Optional[int] = None): embed=discord.Embed( title="Failed", description="Cannot find a message to delete.", - color=discord.Color.red(), + color=self.bot.error_color, ) ) diff --git a/cogs/plugins.py b/cogs/plugins.py index e15b47e29b..556983aaf0 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -7,7 +7,6 @@ import sys import typing import zipfile - from importlib import invalidate_caches from difflib import get_close_matches from pathlib import Path, PurePath @@ -17,6 +16,7 @@ import discord from discord.ext import commands + from pkg_resources import parse_version from core import checks @@ -100,9 +100,13 @@ def __init__(self, bot): self.loaded_plugins = set() self._ready_event = asyncio.Event() - self.bot.loop.create_task(self.initial_load_plugins()) self.bot.loop.create_task(self.populate_registry()) + if self.bot.config.get('enable_plugins'): + self.bot.loop.create_task(self.initial_load_plugins()) + else: + logger.info('Plugins not loaded since ENABLE_PLUGINS=false.') + async def populate_registry(self): url = "https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json" async with self.bot.session.get(url) as resp: @@ -224,7 +228,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): embed = discord.Embed( description="Your bot's version is too low. " f"This plugin requires version `{required_version}`.", - color=discord.Color.red(), + color=self.bot.error_color, ) await ctx.send(embed=embed) return @@ -239,7 +243,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): description="Invalid plugin name, double check the plugin name " "or use one of the following formats: " "username/repo/plugin, username/repo/plugin@branch.", - color=discord.Color.red(), + color=self.bot.error_color, ) await ctx.send(embed=embed) return @@ -270,7 +274,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): if str(plugin) in self.bot.config["plugins"]: embed = discord.Embed( description="This plugin is already installed.", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -278,7 +282,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): # another class with the same name embed = discord.Embed( description="Cannot install this plugin (dupe cog name).", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -296,34 +300,45 @@ async def plugins_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description=f"Failed to download plugin, check logs for error.", - color=discord.Color.red() + color=self.bot.error_color ) return await msg.edit(embed=embed) - invalidate_caches() + self.bot.config["plugins"].append(str(plugin)) + await self.bot.config.update() - try: - await self.load_plugin(plugin) - except Exception: - logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) + if self.bot.config.get('enable_plugins'): - embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", - color=discord.Color.red() - ) + invalidate_caches() - return await msg.edit(embed=embed) + try: + await self.load_plugin(plugin) + except Exception: + logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) - self.bot.config["plugins"].append(str(plugin)) - await self.bot.config.update() + embed = discord.Embed( + description=f"Failed to download plugin, check logs for error.", + color=self.bot.error_color + ) - embed = discord.Embed( - description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*", - color=self.bot.main_color, - ) + return await msg.edit(embed=embed) + + embed = discord.Embed( + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*", + color=self.bot.main_color, + ) + else: + embed = discord.Embed( + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*\n\n" + 'This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, ' + 'to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.', + color=self.bot.main_color, + ) return await msg.edit(embed=embed) @plugins.command(name="remove", aliases=["del", "delete"]) @@ -342,21 +357,22 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( description="Plugin is not installed.", - color=discord.Color.red(), + color=self.bot.error_color ) return await ctx.send(embed=embed) - try: - self.bot.unload_extension(plugin.ext_string) - self.loaded_plugins.remove(plugin) - except (commands.ExtensionNotLoaded, KeyError): - logger.warning("Plugin was never loaded.") + if self.bot.config.get('enable_plugins'): + try: + self.bot.unload_extension(plugin.ext_string) + self.loaded_plugins.remove(plugin) + except (commands.ExtensionNotLoaded, KeyError): + logger.warning("Plugin was never loaded.") self.bot.config["plugins"].remove(str(plugin)) await self.bot.config.update() embed = discord.Embed( - description="The plugin is uninstalled and all its data is erased.", + description="The plugin is successfully uninstalled.", color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -378,17 +394,18 @@ async def plugins_update(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( description="Plugin is not installed.", - color=discord.Color.red(), + color=self.bot.error_color ) return await ctx.send(embed=embed) async with ctx.typing(): await self.download_plugin(plugin, force=True) - try: - self.bot.unload_extension(plugin.ext_string) - except commands.ExtensionError: - logger.warning("Plugin unload fail.", exc_info=True) - await self.load_plugin(plugin) + if self.bot.config.get('enable_plugins'): + try: + self.bot.unload_extension(plugin.ext_string) + except commands.ExtensionError: + logger.warning("Plugin unload fail.", exc_info=True) + await self.load_plugin(plugin) embed = discord.Embed( description=f"Successfully updated {plugin.name}.", @@ -403,6 +420,14 @@ async def plugins_loaded(self, ctx): Show a list of currently loaded plugins. """ + if not self.bot.config.get('enable_plugins'): + embed = discord.Embed( + description="No plugins are loaded due to `ENABLE_PLUGINS=false`, " + 'to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.', + color=self.bot.error_color + ) + return await ctx.send(embed=embed) + if not self._ready_event.is_set(): embed = discord.Embed( description="Plugins are still loading, please try again later.", @@ -413,7 +438,7 @@ async def plugins_loaded(self, ctx): if not self.loaded_plugins: embed = discord.Embed( description="There are no plugins currently loaded.", - color=discord.Color.red() + color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -474,7 +499,7 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N if not index and plugin_name is not None: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f'Could not find a plugin with name "{plugin_name}" within the registry.', ) diff --git a/cogs/utility.py b/cogs/utility.py index d54ad362b9..b62cfb722e 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -14,7 +14,7 @@ from types import SimpleNamespace from typing import Union -from discord import Embed, Color, Activity, Role +import discord from discord.enums import ActivityType, Status from discord.ext import commands, tasks from discord.utils import escape_markdown, escape_mentions @@ -64,7 +64,7 @@ async def format_cog_help(self, cog, *, no_cog=False): if not no_cog else "Miscellaneous commands without a category." ) - embed = Embed(description=f"*{description}*", color=bot.main_color) + embed = discord.Embed(description=f"*{description}*", color=bot.main_color) embed.add_field(name="Commands", value=format_ or "No commands.") @@ -128,7 +128,7 @@ async def _get_help_embed(self, topic): else: perm_level = "NONE" - embed = Embed( + embed = discord.Embed( title=f"`{self.get_command_signature(topic)}`", color=self.context.bot.main_color, description=self.process_help_msg(topic.help), @@ -183,13 +183,13 @@ async def send_error_message(self, error): values = utils.parse_alias(val) if len(values) == 1: - embed = Embed( + embed = discord.Embed( title=f"{command} is an alias.", color=self.context.bot.main_color, description=f"`{command}` points to `{escape_markdown(values[0])}`.", ) else: - embed = Embed( + embed = discord.Embed( title=f"{command} is an alias.", color=self.context.bot.main_color, description=f"**`{command}` points to the following steps:**", @@ -204,7 +204,7 @@ async def send_error_message(self, error): logger.warning("CommandNotFound: %s", str(error)) - embed = Embed(color=Color.red()) + embed = discord.Embed(color=self.context.bot.error_color) embed.set_footer(text=f'Command/Category "{command}" not found.') choices = set() @@ -258,8 +258,8 @@ async def changelog(self, ctx, version: str.lower = ""): index = [v.version for v in changelog.versions].index(version) except ValueError: return await ctx.send( - embed=Embed( - color=Color.red(), + embed=discord.Embed( + color=self.bot.error_color, description=f"The specified version `{version}` could not be found.", ) ) @@ -285,7 +285,7 @@ async def changelog(self, ctx, version: str.lower = ""): @trigger_typing async def about(self, ctx): """Shows information about this bot.""" - embed = Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) + embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) embed.set_author( name="Modmail - About", icon_url=self.bot.user.avatar_url, @@ -301,27 +301,29 @@ async def about(self, ctx): embed.add_field(name="Uptime", value=self.bot.uptime) embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") embed.add_field(name="Version", value=f"`{self.bot.version}`") - embed.add_field(name="Author", value="[`kyb3r`, `Taki`, `4jr`](https://github.com/kyb3r)") + embed.add_field(name="Authors", value="`kyb3r`, `Taki`, `fourjr`") changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version if self.bot.version < parse_version(latest.version): - footer = f"A newer version is available v{latest.version}" + footer = f"A newer version is available v{latest.version}." else: footer = "You are up to date with the latest version." embed.add_field( name="Want Modmail in Your Server?", - value="Installation guide on GitHub (https://github.com/kyb3r/modmail) " - "and join our discord server (https://discord.gg/F34cRU8)!", inline=False + value="Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " + "and join our [Discord server](https://discord.gg/F34cRU8/)!", + inline=False ) embed.add_field( name="Support the Developers", value="This bot is completely free for everyone. We rely on kind individuals " - "like you to support us on [`Patreon`](https://patreon.com/kyber) (includes perks!) " + "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " "to keep this bot free forever!", + inline=False ) embed.set_footer(text=footer) @@ -340,7 +342,7 @@ async def sponsors(self, ctx): embeds = [] for elem in data: - embed = Embed.from_dict(elem["embed"]) + embed = discord.Embed.from_dict(elem["embed"]) embeds.append(embed) random.shuffle(embeds) @@ -366,7 +368,7 @@ async def debug(self, ctx): logs = f.read().strip() if not logs: - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, title="Debug Logs:", description="You don't have any logs at the moment.", @@ -396,7 +398,7 @@ async def debug(self, ctx): msg += "```" messages.append(msg) - embed = Embed(color=self.bot.main_color) + embed = discord.Embed(color=self.bot.main_color) embed.set_footer(text="Debug logs - Navigate using the reactions below.") session = MessagePaginatorSession(ctx, *messages, embed=embed) @@ -431,13 +433,13 @@ async def debug_hastebin(self, ctx): except KeyError: logger.error(data["message"]) raise - embed = Embed( + embed = discord.Embed( title="Debug Logs", color=self.bot.main_color, description=f"{haste_url}/" + key, ) except (JSONDecodeError, ClientResponseError, IndexError, KeyError): - embed = Embed( + embed = discord.Embed( title="Debug Logs", color=self.bot.main_color, description="Something's wrong. " @@ -463,7 +465,7 @@ async def debug_clear(self, ctx): ): pass await ctx.send( - embed=Embed( + embed=discord.Embed( color=self.bot.main_color, description="Cached logs are now cleared." ) ) @@ -495,7 +497,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config.remove("activity_message") await self.bot.config.update() await self.set_presence() - embed = Embed(title="Activity Removed", color=self.bot.main_color) + embed = discord.Embed(title="Activity Removed", color=self.bot.main_color) return await ctx.send(embed=embed) if not message: @@ -515,7 +517,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config["activity_message"] = message await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Activity Changed", description=msg, color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -540,7 +542,7 @@ async def status(self, ctx, *, status_type: str.lower): self.bot.config.remove("status") await self.bot.config.update() await self.set_presence() - embed = Embed(title="Status Removed", color=self.bot.main_color) + embed = discord.Embed(title="Status Removed", color=self.bot.main_color) return await ctx.send(embed=embed) status_type = status_type.replace(" ", "_") @@ -553,7 +555,7 @@ async def status(self, ctx, *, status_type: str.lower): self.bot.config["status"] = status.value await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Status Changed", description=msg, color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -615,7 +617,7 @@ async def set_presence( url = self.bot.config["twitch_url"] if activity_message: - activity = Activity(type=activity_type, name=activity_message, url=url) + activity = discord.Activity(type=activity_type, name=activity_message, url=url) else: msg = "You must supply an activity message to use custom activity." logger.debug(msg) @@ -657,7 +659,7 @@ async def before_loop_presence(self): @trigger_typing async def ping(self, ctx): """Pong! Returns your websocket latency.""" - embed = Embed( + embed = discord.Embed( title="Pong! Websocket Latency:", description=f"{self.bot.ws.latency * 1000:.4f} ms", color=self.bot.main_color, @@ -676,13 +678,13 @@ async def mention(self, ctx, *, mention: str = None): current = self.bot.config["mention"] if mention is None: - embed = Embed( + embed = discord.Embed( title="Current mention:", color=self.bot.main_color, description=str(current), ) else: - embed = Embed( + embed = discord.Embed( title="Changed mention!", description=f'On thread creation the bot now says "{mention}".', color=self.bot.main_color, @@ -702,7 +704,7 @@ async def prefix(self, ctx, *, prefix=None): """ current = self.bot.prefix - embed = Embed( + embed = discord.Embed( title="Current prefix", color=self.bot.main_color, description=f"{current}" ) @@ -744,7 +746,7 @@ async def config_options(self, ctx): description = "\n".join( f"`{name}`" for name in takewhile(lambda x: x is not None, names) ) - embed = Embed( + embed = discord.Embed( title="Available configuration keys:", color=self.bot.main_color, description=description, @@ -765,7 +767,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): try: self.bot.config.set(key, value) await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"Set `{key}` to `{self.bot.config[key]}`.", @@ -773,12 +775,12 @@ async def config_set(self, ctx, key: str.lower, *, value: str): except InvalidConfigError as exc: embed = exc.embed else: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"{key} is an invalid key.", ) - valid_keys = [f"`{k}`" for k in keys] + valid_keys = [f"`{k}`" for k in sorted(keys)] embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -791,18 +793,18 @@ async def config_remove(self, ctx, *, key: str.lower): if key in keys: self.bot.config.remove(key) await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"`{key}` had been reset to default.", ) else: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"{key} is an invalid key.", ) - valid_keys = [f"`{k}`" for k in keys] + valid_keys = [f"`{k}`" for k in sorted(keys)] embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -820,15 +822,15 @@ async def config_get(self, ctx, *, key: str.lower = None): if key: if key in keys: desc = f"`{key}` is set to `{self.bot.config[key]}`" - embed = Embed(color=self.bot.main_color, description=desc) + embed = discord.Embed(color=self.bot.main_color, description=desc) embed.set_author( name="Config variable", icon_url=self.bot.user.avatar_url ) else: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"`{key}` is an invalid key.", ) embed.set_footer( @@ -836,7 +838,7 @@ async def config_get(self, ctx, *, key: str.lower = None): ) else: - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, description="Here is a list of currently " "set configuration variable(s).", @@ -861,9 +863,9 @@ async def config_help(self, ctx, key: str.lower = None): if key is not None and not ( key in self.bot.config.public_keys or key in self.bot.config.protected_keys ): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"`{key}` is an invalid key.", ) return await ctx.send(embed=embed) @@ -871,9 +873,9 @@ async def config_help(self, ctx, key: str.lower = None): config_help = self.bot.config.config_help if key is not None and key not in config_help: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"No help details found for `{key}`.", ) return await ctx.send(embed=embed) @@ -886,7 +888,7 @@ def fmt(val): for i, (current_key, info) in enumerate(config_help.items()): if current_key == key: index = i - embed = Embed( + embed = discord.Embed( title=f"Configuration description on {current_key}:", color=self.bot.main_color, ) @@ -950,9 +952,9 @@ async def alias(self, ctx, *, name: str.lower = None): values = utils.parse_alias(val) if not values: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"Alias `{name}` is invalid, it used to be `{escape_markdown(val)}`. " f"This alias will now be deleted.", ) @@ -961,12 +963,12 @@ async def alias(self, ctx, *, name: str.lower = None): return await ctx.send(embed=embed) if len(values) == 1: - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, description=f"`{name}` points to `{escape_markdown(values[0])}`.", ) else: - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, description=f"**`{name}` points to the following steps:**", ) @@ -976,8 +978,8 @@ async def alias(self, ctx, *, name: str.lower = None): return await ctx.send(embed=embed) if not self.bot.aliases: - embed = Embed( - color=Color.red(), + embed = discord.Embed( + color=self.bot.error_color, description="You dont have any aliases at the moment.", ) embed.set_footer(text=f"Do {self.bot.prefix}help alias for more commands.") @@ -988,7 +990,7 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) - embed = Embed(color=self.bot.main_color, description=description) + embed =discord.Embed(color=self.bot.main_color, description=description) embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) embeds.append(embed) @@ -1025,30 +1027,30 @@ async def alias_add(self, ctx, name: str.lower, *, value): """ embed = None if self.bot.get_command(name): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"A command with the same name already exists: `{name}`.", ) elif name in self.bot.aliases: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"Another alias with the same name already exists: `{name}`.", ) elif name in self.bot.snippets: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"A snippet with the same name already exists: `{name}`.", ) elif len(name) > 120: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"Alias names cannot be longer than 120 characters.", ) @@ -1058,9 +1060,9 @@ async def alias_add(self, ctx, name: str.lower, *, value): values = utils.parse_alias(value) if not values: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="Invalid multi-step alias, try wrapping each steps in quotes.", ) embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') @@ -1069,22 +1071,22 @@ async def alias_add(self, ctx, name: str.lower, *, value): if len(values) == 1: linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="The command you are attempting to point " f"to does not exist: `{linked_command}`.", ) return await ctx.send(embed=embed) - embed = Embed( + embed = discord.Embed( title="Added alias", color=self.bot.main_color, description=f'`{name}` points to: "{values[0]}".', ) else: - embed = Embed( + embed = discord.Embed( title="Added alias", color=self.bot.main_color, description=f"`{name}` now points to the following steps:", @@ -1093,9 +1095,9 @@ async def alias_add(self, ctx, name: str.lower, *, value): for i, val in enumerate(values, start=1): linked_command = val.split()[0] if not self.bot.get_command(linked_command): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="The command you are attempting to point " f"to on step {i} does not exist: `{linked_command}`.", ) @@ -1116,7 +1118,7 @@ async def alias_remove(self, ctx, *, name: str.lower): self.bot.aliases.pop(name) await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Removed alias", color=self.bot.main_color, description=f"Successfully deleted `{name}`.", @@ -1139,9 +1141,9 @@ async def alias_edit(self, ctx, name: str.lower, *, value): values = utils.parse_alias(value) if not values: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="Invalid multi-step alias, try wrapping each steps in quotes.", ) embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') @@ -1150,21 +1152,21 @@ async def alias_edit(self, ctx, name: str.lower, *, value): if len(values) == 1: linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="The command you are attempting to point " f"to does not exist: `{linked_command}`.", ) return await ctx.send(embed=embed) - embed = Embed( + embed = discord.Embed( title="Edited alias", color=self.bot.main_color, description=f'`{name}` now points to: "{values[0]}".', ) else: - embed = Embed( + embed = discord.Embed( title="Edited alias", color=self.bot.main_color, description=f"`{name}` now points to the following steps:", @@ -1173,9 +1175,9 @@ async def alias_edit(self, ctx, name: str.lower, *, value): for i, val in enumerate(values, start=1): linked_command = val.split()[0] if not self.bot.get_command(linked_command): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="The command you are attempting to point " f"to on step {i} does not exist: `{linked_command}`.", ) @@ -1214,7 +1216,7 @@ async def permissions(self, ctx): @staticmethod def _verify_user_or_role(user_or_role): - if isinstance(user_or_role, Role): + if isinstance(user_or_role, discord.Role): if user_or_role.is_default(): return -1 elif user_or_role in {"everyone", "all"}: @@ -1262,18 +1264,18 @@ async def permissions_override( command = self.bot.get_command(command_name) if command is None: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced command does not exist: `{command_name}`.", ) return await ctx.send(embed=embed) level = self._parse_level(level_name) if level is PermissionLevel.INVALID: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced level does not exist: `{level_name}`.", ) else: @@ -1287,7 +1289,7 @@ async def permissions_override( ] = level.name await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description="Successfully set command permission level for " @@ -1303,7 +1305,7 @@ async def permissions_add( type_: str.lower, name: str, *, - user_or_role: Union[Role, utils.User, str], + user_or_role: Union[discord.Role, utils.User, str], ): """ Add a permission to a command or a permission level. @@ -1333,9 +1335,9 @@ async def permissions_add( check = level is not PermissionLevel.INVALID if not check: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced {type_} does not exist: `{name}`.", ) return await ctx.send(embed=embed) @@ -1350,7 +1352,7 @@ async def permissions_add( if level > PermissionLevel.REGULAR: if value == -1: key = self.bot.modmail_guild.default_role - elif isinstance(user_or_role, Role): + elif isinstance(user_or_role, discord.Role): key = user_or_role else: key = self.bot.modmail_guild.get_member(value) @@ -1360,7 +1362,7 @@ async def permissions_add( key, read_messages=True ) - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"Permission for `{name}` was successfully updated.", @@ -1379,7 +1381,7 @@ async def permissions_remove( type_: str.lower, name: str, *, - user_or_role: Union[Role, utils.User, str] = None, + user_or_role: Union[discord.Role, utils.User, str] = None, ): """ Remove permission to use a command, permission level, or command level override. @@ -1411,9 +1413,9 @@ async def permissions_remove( level = self.bot.config["override_command_level"].get(name) if level is None: perm = self.bot.command_perm(name) - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The command permission level was never overridden: `{name}`, " f"current permission level is {perm.name}.", ) @@ -1422,7 +1424,7 @@ async def permissions_remove( self.bot.config["override_command_level"].pop(name) await self.bot.config.update() perm = self.bot.command_perm(name) - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"Command permission level for `{name}` was successfully restored to {perm.name}.", @@ -1436,9 +1438,9 @@ async def permissions_remove( else: level = self._parse_level(name) if level is PermissionLevel.INVALID: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced level does not exist: `{name}`.", ) return await ctx.send(embed=embed) @@ -1454,7 +1456,7 @@ async def permissions_remove( await self.bot.main_category.set_permissions( self.bot.modmail_guild.default_role, read_messages=False ) - elif isinstance(user_or_role, Role): + elif isinstance(user_or_role, discord.Role): logger.info( "Denying %s access to Modmail category.", user_or_role.name ) @@ -1471,7 +1473,7 @@ async def permissions_remove( member, overwrite=None ) - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"Permission for `{name}` was successfully updated.", @@ -1484,7 +1486,7 @@ def _get_perm(self, ctx, name, type_): else: permissions = self.bot.config["level_permissions"].get(name, []) if not permissions: - embed = Embed( + embed = discord.Embed( title=f"Permission entries for {type_} `{name}`:", description="No permission entries found.", color=self.bot.main_color, @@ -1509,7 +1511,7 @@ def _get_perm(self, ctx, name, type_): else: values.append(str(perm)) - embed = Embed( + embed = discord.Embed( title=f"Permission entries for {type_} `{name}`:", description=", ".join(values), color=self.bot.main_color, @@ -1519,7 +1521,7 @@ def _get_perm(self, ctx, name, type_): @permissions.command(name="get", usage="[@user] or [command/level/override] [name]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get( - self, ctx, user_or_role: Union[Role, utils.User, str], *, name: str = None + self, ctx, user_or_role: Union[discord.Role, utils.User, str], *, name: str = None ): """ View the currently-set permissions. @@ -1584,12 +1586,12 @@ async def permissions_get( ) embeds = [ - Embed( + discord.Embed( title=f"{mention} has permission with the following commands:", description=desc_cmd, color=self.bot.main_color, ), - Embed( + discord.Embed( title=f"{mention} has permission with the following permission levels:", description=desc_level, color=self.bot.main_color, @@ -1614,10 +1616,10 @@ async def permissions_get( embeds = [] if not overrides: embeds.append( - Embed( + discord.Embed( title="Permission Overrides", description="You don't have any command level overrides at the moment.", - color=Color.red(), + color=self.bot.error_color, ) ) else: @@ -1630,7 +1632,7 @@ async def permissions_get( lambda x: x is not None, items ) ) - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, description=description ) embed.set_author( @@ -1646,14 +1648,14 @@ async def permissions_get( level = self.bot.config["override_command_level"].get(name) perm = self.bot.command_perm(name) if level is None: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The command permission level was never overridden: `{name}`, " f"current permission level is {perm.name}.", ) else: - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f'Permission override for command "{name}" is "{perm.name}".', @@ -1676,9 +1678,9 @@ async def permissions_get( check = level is not PermissionLevel.INVALID if not check: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced {user_or_role} does not exist: `{name}`.", ) return await ctx.send(embed=embed) @@ -1717,7 +1719,7 @@ async def oauth(self, ctx): @oauth.command(name="whitelist") @checks.has_permissions(PermissionLevel.OWNER) - async def oauth_whitelist(self, ctx, target: Union[Role, utils.User]): + async def oauth_whitelist(self, ctx, target: Union[discord.Role, utils.User]): """ Whitelist or un-whitelist a user or role to have access to logs. @@ -1735,7 +1737,7 @@ async def oauth_whitelist(self, ctx, target: Union[Role, utils.User]): await self.bot.config.update() - embed = Embed(color=self.bot.main_color) + embed = discord.Embed(color=self.bot.main_color) embed.title = "Success" if not hasattr(target, "mention"): @@ -1766,7 +1768,7 @@ async def oauth_show(self, ctx): if role: roles.append(role) - embed = Embed(color=self.bot.main_color) + embed = discord.Embed(color=self.bot.main_color) embed.title = "Oauth Whitelist" embed.add_field( diff --git a/core/config.py b/core/config.py index f20049e10a..b67aa23329 100644 --- a/core/config.py +++ b/core/config.py @@ -31,6 +31,7 @@ class ConfigManager: "prefix": "?", "mention": "@here", "main_color": str(discord.Color.blurple()), + "error_color": str(discord.Color.red()), "user_typing": False, "mod_typing": False, "account_age": None, @@ -100,10 +101,10 @@ class ConfigManager: "token": None, # Logging "log_level": "INFO", - "load_plugins": True, + "enable_plugins": True, } - colors = {"mod_color", "recipient_color", "main_color"} + colors = {"mod_color", "recipient_color", "main_color", "error_color"} time_deltas = {"account_age", "guild_age", "thread_auto_close"} @@ -114,7 +115,7 @@ class ConfigManager: "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", - "load_plugins" + "enable_plugins" } defaults = {**public_keys, **private_keys, **protected_keys} diff --git a/core/config_help.json b/core/config_help.json index 3b5028ab28..4afcf63405 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -53,10 +53,25 @@ ], "notes": [ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", - "See also: `mod_color`, `recipient_color`." + "See also: `error_color`, `mod_color`, `recipient_color`." ], "thumbnail": "https://placehold.it/100/7289da?text=+" }, + "error_color": { + "default": "Discord Red [#E74C3C](https://placehold.it/100/e74c3c?text=+)", + "description": "This is the color for Modmail when anything goes wrong, unsuccessful commands, or a stern warning.", + "examples": [ + "`{prefix}config set error_color ocean blue`", + "`{prefix}config set error_color ff1242`", + "`{prefix}config set error_color #ff1242`", + "`{prefix}config set error_color fa1`" + ], + "notes": [ + "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", + "See also: `main_color`, `mod_color`, `recipient_color`." + ], + "thumbnail": "https://placehold.it/100/e74c3c?text=+" + }, "user_typing": { "default": "Disabled", "description": "When this is set to `yes`, whenever the recipient user starts to type in their DM channel, the moderator will see “{bot.user.display_name} is typing…” in the thread channel.", @@ -333,7 +348,7 @@ ], "notes": [ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", - "See also: `main_color`, `mod_color`." + "See also: `mod_color`, `main_color`, `error_color`." ], "thumbnail": "https://placehold.it/100/f1c40f?text=+" }, @@ -348,7 +363,7 @@ ], "notes": [ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", - "See also: `main_color`, `recipient_color`." + "See also: `recipient_color`, `main_color`, `error_color`." ], "thumbnail": "https://placehold.it/100/2ecc71?text=+" }, @@ -468,9 +483,9 @@ "This configuration can only to be set through `.env` file or environment (config) variables." ] }, - "load_plugins": { + "enable_plugins": { "default": "Yes", - "description": "Whether plugins should be loaded into Modmail.", + "description": "Whether plugins should be enabled and loaded into Modmail.", "examples": [ ], "notes": [ diff --git a/core/models.py b/core/models.py index 8b21c10eb0..4d240e70c0 100644 --- a/core/models.py +++ b/core/models.py @@ -3,7 +3,7 @@ from enum import IntEnum from string import Formatter -from discord import Color, Embed +import discord from discord.ext import commands try: @@ -31,7 +31,8 @@ def __init__(self, msg, *args): @property def embed(self): - return Embed(title="Error", description=self.msg, color=Color.red()) + # Single reference of Color.red() + return discord.Embed(title="Error", description=self.msg, color=discord.Color.red()) class ModmailLogger(logging.Logger): diff --git a/core/thread.py b/core/thread.py index 9686e5ecb1..915fc30013 100644 --- a/core/thread.py +++ b/core/thread.py @@ -106,7 +106,7 @@ async def setup(self, *, creator=None, category=None): logger.critical("An error occurred while creating a thread.", exc_info=True) self.manager.cache.pop(self.id) - embed = discord.Embed(color=discord.Color.red()) + embed = discord.Embed(color=self.bot.error_color) embed.title = "Error while trying to create a thread." embed.description = str(e) embed.add_field(name="Recipient", value=recipient.mention) @@ -138,7 +138,7 @@ async def setup(self, *, creator=None, category=None): async def send_genesis_message(): info_embed = self._format_info_embed( - recipient, log_url, log_count, discord.Color.green() + recipient, log_url, log_count, self.bot.main_color ) try: msg = await channel.send(mention, embed=info_embed) @@ -350,7 +350,7 @@ async def _close( desc = "Could not resolve log url." log_url = None - embed = discord.Embed(description=desc, color=discord.Color.red()) + embed = discord.Embed(description=desc, color=self.bot.error_color) if self.recipient is not None: user = f"{self.recipient} (`{self.id}`)" @@ -378,7 +378,7 @@ async def _close( embed = discord.Embed( title=self.bot.config["thread_close_title"], - color=discord.Color.red(), + color=self.bot.error_color, timestamp=datetime.utcnow(), ) @@ -528,7 +528,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None if not any(g.get_member(self.id) for g in self.bot.guilds): return await message.channel.send( embed=discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Your message could not be delivered since " "the recipient shares no servers with the bot.", ) @@ -545,7 +545,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None tasks.append( message.channel.send( embed=discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Your message could not be delivered as " "the recipient is only accepting direct " "messages from friends, or the bot was " @@ -578,7 +578,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None tasks.append( self.channel.send( embed=discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Scheduled close has been cancelled.", ) ) @@ -607,7 +607,7 @@ async def send( self.bot.loop.create_task( self.channel.send( embed=discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Scheduled close has been cancelled.", ) ) @@ -696,7 +696,7 @@ async def send( embedded_image = True elif filename is not None: if note: - color = discord.Color.blurple() + color = self.bot.main_color elif from_mod: color = self.bot.mod_color else: @@ -735,7 +735,7 @@ async def send( else: embed.set_footer(text=self.bot.config["anon_tag"]) elif note: - embed.colour = discord.Color.blurple() + embed.colour = self.bot.main_color else: embed.set_footer(text=f"Message ID: {message.id}") embed.colour = self.bot.recipient_color diff --git a/core/utils.py b/core/utils.py index de17e18419..146f6224af 100644 --- a/core/utils.py +++ b/core/utils.py @@ -202,6 +202,7 @@ async def ignore(coro): def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discord.Embed: + # Single reference of Color.red() embed = discord.Embed( color=discord.Color.red(), description=f"**{name.capitalize()} `{word}` cannot be found.**", From 53f930deea0313b91d8e1a9e8845bf7aebe22e27 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 20 Sep 2019 01:11:20 -0700 Subject: [PATCH 06/48] Black formatting --- bot.py | 15 ++-- cogs/modmail.py | 10 ++- cogs/plugins.py | 176 ++++++++++++++++++++++++-------------------- cogs/utility.py | 24 +++--- core/_color_data.py | 6 +- core/changelog.py | 16 +++- core/config.py | 8 +- core/models.py | 7 +- core/thread.py | 10 ++- 9 files changed, 156 insertions(+), 116 deletions(-) diff --git a/bot.py b/bot.py index f4ff2c1ec4..6b6d2b986c 100644 --- a/bot.py +++ b/bot.py @@ -71,7 +71,7 @@ def __init__(self): self._api = None self.metadata_loop = None self.formatter = SafeFormatter() - self.loaded_cogs = ['cogs.modmail', 'cogs.plugins', 'cogs.utility'] + self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -829,7 +829,7 @@ async def process_commands(self, message): thread = await self.threads.find(channel=ctx.channel) if thread is not None: - if self.config.get('reply_without_command'): + if self.config.get("reply_without_command"): await thread.reply(message) else: await self.api.append_log(message, type_="internal") @@ -857,7 +857,7 @@ async def _void(*_args, **_kwargs): if thread: await thread.channel.trigger_typing() else: - if not self.config.get('mod_typing'): + if not self.config.get("mod_typing"): return thread = await self.threads.find(channel=channel) @@ -895,7 +895,7 @@ async def on_raw_reaction_add(self, payload): if isinstance(channel, discord.DMChannel): if str(reaction) == str(close_emoji): # closing thread - if not self.config.get('recipient_thread_close'): + if not self.config.get("recipient_thread_close"): return thread = await self.threads.find(recipient=user) ts = message.embeds[0].timestamp if message.embeds else None @@ -953,8 +953,7 @@ async def on_member_remove(self, member): thread = await self.threads.find(recipient=member) if thread: embed = discord.Embed( - description="The recipient has left the server.", - color=self.error_color, + description="The recipient has left the server.", color=self.error_color ) await thread.channel.send(embed=embed) @@ -1018,9 +1017,7 @@ async def on_command_error(self, context, exception): elif isinstance(exception, commands.BadArgument): await context.trigger_typing() await context.send( - embed=discord.Embed( - color=self.error_color, description=str(exception) - ) + embed=discord.Embed(color=self.error_color, description=str(exception)) ) elif isinstance(exception, commands.CommandNotFound): logger.warning("CommandNotFound: %s", exception) diff --git a/cogs/modmail.py b/cogs/modmail.py index 33d7f28457..1450a405cf 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -286,7 +286,9 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): @commands.command() @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() - async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None): + async def move( + self, ctx, category: discord.CategoryChannel, *, specifics: str = None + ): """ Move a thread to another category. @@ -297,16 +299,16 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = silent = False if specifics: - silent_words = ['silent', 'silently'] + silent_words = ["silent", "silently"] silent = any(word in silent_words for word in specifics.split()) await thread.channel.edit(category=category, sync_permissions=True) - if self.bot.config('thread_move_notify') and not silent: + if self.bot.config("thread_move_notify") and not silent: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], - color=self.bot.main_color + color=self.bot.main_color, ) await thread.recipient.send(embed=embed) diff --git a/cogs/plugins.py b/cogs/plugins.py index 556983aaf0..1f0ccf3a03 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -36,13 +36,15 @@ def __init__(self, user, repo, name, branch=None): self.user = user self.repo = repo self.name = name - self.branch = branch if branch is not None else 'master' + self.branch = branch if branch is not None else "master" self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip" self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}" @property def path(self): - return PurePath('plugins') / self.user / self.repo / f'{self.name}-{self.branch}' + return ( + PurePath("plugins") / self.user / self.repo / f"{self.name}-{self.branch}" + ) @property def abs_path(self): @@ -50,15 +52,19 @@ def abs_path(self): @property def cache_path(self): - return Path(__file__).absolute().parent.parent / 'temp' / \ - 'plugins-cache' / f'{self.user}-{self.repo}-{self.branch}.zip' + return ( + Path(__file__).absolute().parent.parent + / "temp" + / "plugins-cache" + / f"{self.user}-{self.repo}-{self.branch}.zip" + ) @property def ext_string(self): - return f'plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}' + return f"plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}" def __str__(self): - return f'{self.user}/{self.repo}/{self.name}@{self.branch}' + return f"{self.user}/{self.repo}/{self.name}@{self.branch}" def __lt__(self, other): return self.name.lower() < other.name.lower() @@ -66,19 +72,19 @@ def __lt__(self, other): @classmethod def from_string(cls, s, strict=False): if not strict: - m = match(r'^(.+?)/(.+?)/(.+?)(?:@(.+?))?$', s) + m = match(r"^(.+?)/(.+?)/(.+?)(?:@(.+?))?$", s) else: - m = match(r'^(.+?)/(.+?)/(.+?)@(.+?)$', s) + m = match(r"^(.+?)/(.+?)/(.+?)@(.+?)$", s) if m is not None: return Plugin(*m.groups()) else: - raise InvalidPluginError('Cannot decipher %s.', s) + raise InvalidPluginError("Cannot decipher %s.", s) def __hash__(self): return hash((self.user, self.repo, self.name, self.branch)) def __repr__(self): - return f'' + return f"" def __eq__(self, other): return isinstance(other, Plugin) and self.__str__() == other.__str__() @@ -102,10 +108,10 @@ def __init__(self, bot): self.bot.loop.create_task(self.populate_registry()) - if self.bot.config.get('enable_plugins'): + if self.bot.config.get("enable_plugins"): self.bot.loop.create_task(self.initial_load_plugins()) else: - logger.info('Plugins not loaded since ENABLE_PLUGINS=false.') + logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") async def populate_registry(self): url = "https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json" @@ -124,10 +130,14 @@ async def initial_load_plugins(self): # For backwards compat plugin = Plugin.from_string(plugin_name) except InvalidPluginError: - logger.error("Failed to parse plugin name: %s.", plugin_name, exc_info=True) + logger.error( + "Failed to parse plugin name: %s.", plugin_name, exc_info=True + ) continue - logger.info("Migrated legacy plugin name: %s, now %s.", plugin_name, str(plugin)) + logger.info( + "Migrated legacy plugin name: %s, now %s.", plugin_name, str(plugin) + ) self.bot.config["plugins"].append(str(plugin)) try: @@ -148,56 +158,62 @@ async def download_plugin(self, plugin, force=False): plugin.abs_path.mkdir(parents=True, exist_ok=True) if plugin.cache_path.exists() and not force: - plugin_io = plugin.cache_path.open('rb') - logger.debug('Loading cached %s.', plugin.cache_path) + plugin_io = plugin.cache_path.open("rb") + logger.debug("Loading cached %s.", plugin.cache_path) else: async with self.bot.session.get(plugin.url) as resp: - logger.debug('Downloading %s.', plugin.url) + logger.debug("Downloading %s.", plugin.url) raw = await resp.read() plugin_io = io.BytesIO(raw) if not plugin.cache_path.parent.exists(): plugin.cache_path.parent.mkdir(parents=True) - with plugin.cache_path.open('wb') as f: + with plugin.cache_path.open("wb") as f: f.write(raw) with zipfile.ZipFile(plugin_io) as zipf: for member in zipf.namelist(): path = PurePath(member) if len(path.parts) >= 3 and path.parts[1] == plugin.name: - with zipf.open(member) as src, (plugin.abs_path / Path(*path.parts[2:])).open("wb") as dst: + with zipf.open(member) as src, ( + plugin.abs_path / Path(*path.parts[2:]) + ).open("wb") as dst: shutil.copyfileobj(src, dst) plugin_io.close() async def load_plugin(self, plugin): - if not (plugin.abs_path / f'{plugin.name}.py').exists(): - raise InvalidPluginError(f'{plugin.name}.py not found.') + if not (plugin.abs_path / f"{plugin.name}.py").exists(): + raise InvalidPluginError(f"{plugin.name}.py not found.") - req_txt = plugin.abs_path / 'requirements.txt' + req_txt = plugin.abs_path / "requirements.txt" if req_txt.exists(): # Install PIP requirements venv = hasattr(sys, "real_prefix") # in a virtual env - user_install = ' --user' if not venv else '' + user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", stderr=PIPE, - stdout=PIPE + stdout=PIPE, ) - logger.debug('Downloading requirements for %s.', plugin.ext_string) + logger.debug("Downloading requirements for %s.", plugin.ext_string) stdout, stderr = await proc.communicate() if stdout: - logger.debug('[stdout]\n%s.', stdout.decode()) + logger.debug("[stdout]\n%s.", stdout.decode()) if stderr: - logger.debug('[stderr]\n%s.', stderr.decode()) - logger.error("Failed to download requirements for %s.", plugin.ext_string, exc_info=True) + logger.debug("[stderr]\n%s.", stderr.decode()) + logger.error( + "Failed to download requirements for %s.", + plugin.ext_string, + exc_info=True, + ) raise InvalidPluginError( f"Unable to download requirements: ```\n{stderr.decode()}\n```" ) @@ -207,27 +223,29 @@ async def load_plugin(self, plugin): try: self.bot.load_extension(plugin.ext_string) - logger.info('Loaded plugin: %s', plugin.ext_string) + logger.info("Loaded plugin: %s", plugin.ext_string) self.loaded_plugins.add(plugin) except commands.ExtensionError as exc: - logger.error('Plugin load failure: %s', plugin.ext_string, exc_info=True) + logger.error("Plugin load failure: %s", plugin.ext_string, exc_info=True) raise InvalidPluginError("Cannot load extension, plugin invalid.") from exc async def parse_user_input(self, ctx, plugin_name, check_version=False): if plugin_name in self.registry: details = self.registry[plugin_name] - user, repo = details["repository"].split('/', maxsplit=1) + user, repo = details["repository"].split("/", maxsplit=1) branch = details.get("branch") if check_version: required_version = details.get("bot_version", False) - if required_version and self.bot.version < parse_version(required_version): + if required_version and self.bot.version < parse_version( + required_version + ): embed = discord.Embed( description="Your bot's version is too low. " - f"This plugin requires version `{required_version}`.", + f"This plugin requires version `{required_version}`.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -241,8 +259,8 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): except InvalidPluginError: embed = discord.Embed( description="Invalid plugin name, double check the plugin name " - "or use one of the following formats: " - "username/repo/plugin, username/repo/plugin@branch.", + "or use one of the following formats: " + "username/repo/plugin, username/repo/plugin@branch.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -288,7 +306,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description=f"Starting to download plugin from {plugin.link}...", - color=self.bot.main_color + color=self.bot.main_color, ) msg = await ctx.send(embed=embed) @@ -300,7 +318,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description=f"Failed to download plugin, check logs for error.", - color=self.bot.error_color + color=self.bot.error_color, ) return await msg.edit(embed=embed) @@ -308,7 +326,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): self.bot.config["plugins"].append(str(plugin)) await self.bot.config.update() - if self.bot.config.get('enable_plugins'): + if self.bot.config.get("enable_plugins"): invalidate_caches() @@ -319,24 +337,24 @@ async def plugins_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description=f"Failed to download plugin, check logs for error.", - color=self.bot.error_color + color=self.bot.error_color, ) return await msg.edit(embed=embed) embed = discord.Embed( description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*", + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*", color=self.bot.main_color, ) else: embed = discord.Embed( description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*\n\n" - 'This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, ' - 'to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.', + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*\n\n" + "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", color=self.bot.main_color, ) return await msg.edit(embed=embed) @@ -356,12 +374,11 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description="Plugin is not installed.", - color=self.bot.error_color + description="Plugin is not installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) - if self.bot.config.get('enable_plugins'): + if self.bot.config.get("enable_plugins"): try: self.bot.unload_extension(plugin.ext_string) self.loaded_plugins.remove(plugin) @@ -393,14 +410,13 @@ async def plugins_update(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description="Plugin is not installed.", - color=self.bot.error_color + description="Plugin is not installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) async with ctx.typing(): await self.download_plugin(plugin, force=True) - if self.bot.config.get('enable_plugins'): + if self.bot.config.get("enable_plugins"): try: self.bot.unload_extension(plugin.ext_string) except commands.ExtensionError: @@ -420,47 +436,45 @@ async def plugins_loaded(self, ctx): Show a list of currently loaded plugins. """ - if not self.bot.config.get('enable_plugins'): + if not self.bot.config.get("enable_plugins"): embed = discord.Embed( description="No plugins are loaded due to `ENABLE_PLUGINS=false`, " - 'to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.', - color=self.bot.error_color + "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.", + color=self.bot.error_color, ) return await ctx.send(embed=embed) if not self._ready_event.is_set(): embed = discord.Embed( description="Plugins are still loading, please try again later.", - color=self.bot.main_color + color=self.bot.main_color, ) return await ctx.send(embed=embed) if not self.loaded_plugins: embed = discord.Embed( description="There are no plugins currently loaded.", - color=self.bot.error_color + color=self.bot.error_color, ) return await ctx.send(embed=embed) loaded_plugins = map(str, sorted(self.loaded_plugins)) - pages = ['```\n'] + pages = ["```\n"] for plugin in loaded_plugins: - msg = str(plugin) + '\n' + msg = str(plugin) + "\n" if len(msg) + len(pages[-1]) + 3 <= 2048: pages[-1] += msg else: - pages[-1] += '```' - pages.append(f'```\n{msg}') + pages[-1] += "```" + pages.append(f"```\n{msg}") - if pages[-1][-3:] != '```': - pages[-1] += '```' + if pages[-1][-3:] != "```": + pages[-1] += "```" embeds = [] for page in pages: embed = discord.Embed( - title="Loaded plugins:", - description=page, - color=self.bot.main_color + title="Loaded plugins:", description=page, color=self.bot.main_color ) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) @@ -470,7 +484,9 @@ async def plugins_loaded(self, ctx): invoke_without_command=True, name="registry", aliases=["list", "info"] ) @checks.has_permissions(PermissionLevel.OWNER) - async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None): + async def plugins_registry( + self, ctx, *, plugin_name: typing.Union[int, str] = None + ): """ Shows a list of all approved plugins. @@ -515,7 +531,7 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N for plugin_name, details in registry: details = self.registry[plugin_name] - user, repo = details["repository"].split('/', maxsplit=1) + user, repo = details["repository"].split("/", maxsplit=1) branch = details.get("branch") plugin = Plugin(user, repo, plugin_name, branch) @@ -528,13 +544,12 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N ) embed.add_field( - name="Installation", value=f"```{self.bot.prefix}plugins add {plugin_name}```" + name="Installation", + value=f"```{self.bot.prefix}plugins add {plugin_name}```", ) embed.set_author( - name=details["title"], - icon_url=details.get("icon_url"), - url=plugin.link + name=details["title"], icon_url=details.get("icon_url"), url=plugin.link ) if details.get("thumbnail_url"): @@ -547,10 +562,13 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N embed.set_footer(text="This plugin is currently loaded.") else: required_version = details.get("bot_version", False) - if required_version and self.bot.version < parse_version(required_version): + if required_version and self.bot.version < parse_version( + required_version + ): embed.set_footer( text=f"Your bot is unable to install this plugin, " - f"minimum required version is v{required_version}.") + f"minimum required version is v{required_version}." + ) else: embed.set_footer(text="Your bot is able to install this plugin.") @@ -571,16 +589,18 @@ async def plugins_registry_compact(self, ctx): registry = sorted(self.registry.items(), key=lambda elem: elem[0]) - pages = [''] + pages = [""] for plugin_name, details in registry: details = self.registry[plugin_name] - user, repo = details["repository"].split('/', maxsplit=1) + user, repo = details["repository"].split("/", maxsplit=1) branch = details.get("branch") plugin = Plugin(user, repo, plugin_name, branch) - desc = discord.utils.escape_markdown(details["description"].replace("\n", "")) + desc = discord.utils.escape_markdown( + details["description"].replace("\n", "") + ) name = f"[`{plugin.name}`]({plugin.link})" fmt = f"{name} - {desc}" @@ -590,13 +610,13 @@ async def plugins_registry_compact(self, ctx): if limit < 0: fmt = plugin.name limit = 75 - fmt = truncate(fmt, limit) + '[loaded]\n' + fmt = truncate(fmt, limit) + "[loaded]\n" else: limit = 75 - len(plugin_name) - 4 + len(name) if limit < 0: fmt = plugin.name limit = 75 - fmt = truncate(fmt, limit) + '\n' + fmt = truncate(fmt, limit) + "\n" if len(fmt) + len(pages[-1]) <= 2048: pages[-1] += fmt diff --git a/cogs/utility.py b/cogs/utility.py index b62cfb722e..d9fdde9837 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -314,16 +314,16 @@ async def about(self, ctx): embed.add_field( name="Want Modmail in Your Server?", value="Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " - "and join our [Discord server](https://discord.gg/F34cRU8/)!", - inline=False + "and join our [Discord server](https://discord.gg/F34cRU8/)!", + inline=False, ) embed.add_field( name="Support the Developers", value="This bot is completely free for everyone. We rely on kind individuals " - "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " - "to keep this bot free forever!", - inline=False + "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " + "to keep this bot free forever!", + inline=False, ) embed.set_footer(text=footer) @@ -617,7 +617,9 @@ async def set_presence( url = self.bot.config["twitch_url"] if activity_message: - activity = discord.Activity(type=activity_type, name=activity_message, url=url) + activity = discord.Activity( + type=activity_type, name=activity_message, url=url + ) else: msg = "You must supply an activity message to use custom activity." logger.debug(msg) @@ -643,7 +645,7 @@ async def loop_presence(self): """Set presence to the configured value every 45 minutes.""" # TODO: Does this even work? presence = await self.set_presence() - logger.debug('Loop... %s - %s', presence["activity"][1], presence["status"][1]) + logger.debug("Loop... %s - %s", presence["activity"][1], presence["status"][1]) @loop_presence.before_loop async def before_loop_presence(self): @@ -990,7 +992,7 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) - embed =discord.Embed(color=self.bot.main_color, description=description) + embed = discord.Embed(color=self.bot.main_color, description=description) embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) embeds.append(embed) @@ -1521,7 +1523,11 @@ def _get_perm(self, ctx, name, type_): @permissions.command(name="get", usage="[@user] or [command/level/override] [name]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get( - self, ctx, user_or_role: Union[discord.Role, utils.User, str], *, name: str = None + self, + ctx, + user_or_role: Union[discord.Role, utils.User, str], + *, + name: str = None, ): """ View the currently-set permissions. diff --git a/core/_color_data.py b/core/_color_data.py index acfd834505..ad98d3856f 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -39,11 +39,13 @@ "light gray": "979c9f", "dark gray": "607d8b", "blurple": "7289da", - "grayple": "99aab5" + "grayple": "99aab5", } # Normalize name to "discord:" to avoid name collisions. -DISCORD_COLORS_NORM = {"discord:" + name: value for name, value in DISCORD_COLORS.items()} +DISCORD_COLORS_NORM = { + "discord:" + name: value for name, value in DISCORD_COLORS.items() +} # These colors are from Tableau diff --git a/core/changelog.py b/core/changelog.py index f687cfa57a..3e48a41f97 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -51,7 +51,9 @@ def __init__(self, bot, branch: str, version: str, lines: str): self.version = version.lstrip("vV") self.lines = lines.strip() self.fields = {} - self.changelog_url = f"https://github.com/kyb3r/modmail/blob/{branch}/CHANGELOG.md" + self.changelog_url = ( + f"https://github.com/kyb3r/modmail/blob/{branch}/CHANGELOG.md" + ) self.description = "" self.parse() @@ -126,6 +128,7 @@ class Changelog: VERSION_REGEX : re.Pattern The regex used to parse the versions. """ + VERSION_REGEX = re.compile( r"#\s*([vV]\d+\.\d+(?:\.\d+)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)?|$)", flags=re.DOTALL, @@ -135,7 +138,9 @@ def __init__(self, bot, branch: str, text: str): self.bot = bot self.text = text logger.debug("Fetching changelog from GitHub.") - self.versions = [Version(bot, branch, *m) for m in self.VERSION_REGEX.findall(text)] + self.versions = [ + Version(bot, branch, *m) for m in self.VERSION_REGEX.findall(text) + ] @property def latest_version(self) -> Version: @@ -168,8 +173,11 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": Changelog The newly created `Changelog` parsed from the `url`. """ - branch = 'master' if not bot.version.is_prerelease else 'development' - url = url or f"https://raw.githubusercontent.com/kyb3r/modmail/{branch}/CHANGELOG.md" + branch = "master" if not bot.version.is_prerelease else "development" + url = ( + url + or f"https://raw.githubusercontent.com/kyb3r/modmail/{branch}/CHANGELOG.md" + ) async with await bot.session.get(url) as resp: return cls(bot, branch, await resp.text()) diff --git a/core/config.py b/core/config.py index b67aa23329..0230a923b3 100644 --- a/core/config.py +++ b/core/config.py @@ -115,7 +115,7 @@ class ConfigManager: "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", - "enable_plugins" + "enable_plugins", } defaults = {**public_keys, **private_keys, **protected_keys} @@ -263,7 +263,7 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: hex_ = ALL_COLORS.get(name) if hex_ is None: raise - return self.__setitem__(key, '#' + hex_) + return self.__setitem__(key, "#" + hex_) if key in self.time_deltas: try: @@ -271,7 +271,9 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: except isodate.ISO8601Error: try: converter = UserFriendlyTime() - time = self.bot.loop.run_until_complete(converter.convert(None, item)) + time = self.bot.loop.run_until_complete( + converter.convert(None, item) + ) if time.arg: raise ValueError except BadArgument as exc: diff --git a/core/models.py b/core/models.py index 4d240e70c0..033457a542 100644 --- a/core/models.py +++ b/core/models.py @@ -32,7 +32,9 @@ def __init__(self, msg, *args): @property def embed(self): # Single reference of Color.red() - return discord.Embed(title="Error", description=self.msg, color=discord.Color.red()) + return discord.Embed( + title="Error", description=self.msg, color=discord.Color.red() + ) class ModmailLogger(logging.Logger): @@ -91,7 +93,6 @@ class _Default: class SafeFormatter(Formatter): - def get_field(self, field_name, args, kwargs): first, rest = _string.formatter_field_name_split(field_name) @@ -108,7 +109,7 @@ def get_field(self, field_name, args, kwargs): if n >= 2: break if is_attr: - if str(i).startswith('_'): + if str(i).startswith("_"): break obj = getattr(obj, i) else: diff --git a/core/thread.py b/core/thread.py index 915fc30013..ce25e49652 100644 --- a/core/thread.py +++ b/core/thread.py @@ -389,7 +389,10 @@ async def _close( message = self.bot.config["thread_close_response"] message = self.bot.formatter.format( - message, closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None + message, + closer=closer, + loglink=log_url, + logkey=log_data["key"] if log_data else None, ) embed.description = message @@ -459,15 +462,14 @@ async def _restart_close_timer(self): reset_time = datetime.utcnow() + timedelta(seconds=seconds) human_time = human_timedelta(dt=reset_time) - if self.bot.config.get('thread_auto_close_silently'): + if self.bot.config.get("thread_auto_close_silently"): return await self.close( closer=self.bot.user, silent=True, after=int(seconds), auto_close=True ) # Grab message close_message = self.bot.formatter.format( - self.bot.config["thread_auto_close_response"], - timeout=human_time + self.bot.config["thread_auto_close_response"], timeout=human_time ) time_marker_regex = "%t" From cf08042337326c60cdee2004bd502ff3921e6cba Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 20 Sep 2019 10:18:12 -0700 Subject: [PATCH 07/48] Fixed a plugin issue when downloading --- cogs/plugins.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 1f0ccf3a03..1fbc9e8aeb 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -173,13 +173,16 @@ async def download_plugin(self, plugin, force=False): f.write(raw) with zipfile.ZipFile(plugin_io) as zipf: - for member in zipf.namelist(): - path = PurePath(member) + for info in zipf.infolist(): + path = PurePath(info.filename) if len(path.parts) >= 3 and path.parts[1] == plugin.name: - with zipf.open(member) as src, ( - plugin.abs_path / Path(*path.parts[2:]) - ).open("wb") as dst: - shutil.copyfileobj(src, dst) + plugin_path = plugin.abs_path / Path(*path.parts[2:]) + if info.is_dir(): + plugin_path.mkdir(parents=True, exist_ok=True) + else: + plugin_path.parent.mkdir(parents=True, exist_ok=True) + with zipf.open(info) as src, plugin_path.open("wb") as dst: + shutil.copyfileobj(src, dst) plugin_io.close() From efae94216bf9fe8703f96585885fe88090d45f77 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 20 Sep 2019 21:40:33 -0700 Subject: [PATCH 08/48] Remove dir when plugin remove --- CHANGELOG.md | 3 +- cogs/modmail.py | 3 +- cogs/plugins.py | 82 +++++++++++++++++++++++++++------------------- cogs/utility.py | 15 ++++----- core/decorators.py | 14 ++++---- core/utils.py | 10 ++++++ 6 files changed, 75 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dde8659b0b..ba0edfa116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting. - To get/set the raw value, access through `config[]`. -- Prerelease naming scheme is now `x.x.x-dev`. +- Prerelease naming scheme is now `x.x.x-devN`. +- `trigger_typing` has been moved to `core.utils.trigger_typing`, original location is deprecated. # v3.2.2 diff --git a/cogs/modmail.py b/cogs/modmail.py index 1450a405cf..b81f445b11 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -13,11 +13,10 @@ from natural.date import duration from core import checks -from core.decorators import trigger_typing from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed, format_description +from core.utils import format_preview, User, create_not_found_embed, format_description, trigger_typing logger = logging.getLogger("Modmail") diff --git a/cogs/plugins.py b/cogs/plugins.py index 1fbc9e8aeb..3adbd8c3bb 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -22,7 +22,7 @@ from core import checks from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession -from core.utils import truncate +from core.utils import truncate, trigger_typing logger = logging.getLogger("Modmail") @@ -235,6 +235,14 @@ async def load_plugin(self, plugin): async def parse_user_input(self, ctx, plugin_name, check_version=False): + if not self._ready_event.is_set(): + embed = discord.Embed( + description="Plugins are still loading, please try again later.", + color=self.bot.main_color, + ) + await ctx.send(embed=embed) + return + if plugin_name in self.registry: details = self.registry[plugin_name] user, repo = details["repository"].split("/", maxsplit=1) @@ -281,6 +289,7 @@ async def plugins(self, ctx): @plugins.command(name="add", aliases=["install", "load"]) @checks.has_permissions(PermissionLevel.OWNER) + @trigger_typing async def plugins_add(self, ctx, *, plugin_name: str): """ Install a new plugin for the bot. @@ -313,53 +322,51 @@ async def plugins_add(self, ctx, *, plugin_name: str): ) msg = await ctx.send(embed=embed) - async with ctx.typing(): - try: - await self.download_plugin(plugin, force=True) - except Exception: - logger.warning(f"Unable to download plugin %s.", plugin, exc_info=True) - - embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", - color=self.bot.error_color, - ) - - return await msg.edit(embed=embed) + try: + await self.download_plugin(plugin, force=True) + except Exception: + logger.warning(f"Unable to download plugin %s.", plugin, exc_info=True) - self.bot.config["plugins"].append(str(plugin)) - await self.bot.config.update() + embed = discord.Embed( + description=f"Failed to download plugin, check logs for error.", + color=self.bot.error_color, + ) - if self.bot.config.get("enable_plugins"): + return await msg.edit(embed=embed) - invalidate_caches() + self.bot.config["plugins"].append(str(plugin)) + await self.bot.config.update() - try: - await self.load_plugin(plugin) - except Exception: - logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) + if self.bot.config.get("enable_plugins"): - embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", - color=self.bot.error_color, - ) + invalidate_caches() - return await msg.edit(embed=embed) + try: + await self.load_plugin(plugin) + except Exception: + logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*", - color=self.bot.main_color, + description=f"Failed to download plugin, check logs for error.", + color=self.bot.error_color, ) + else: embed = discord.Embed( description="Successfully installed plugin.\n" "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*\n\n" - "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", + "Please only install plugins from developers you trust.*", color=self.bot.main_color, ) + else: + embed = discord.Embed( + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*\n\n" + "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", + color=self.bot.main_color, + ) return await msg.edit(embed=embed) @plugins.command(name="remove", aliases=["del", "delete"]) @@ -390,6 +397,15 @@ async def plugins_remove(self, ctx, *, plugin_name: str): self.bot.config["plugins"].remove(str(plugin)) await self.bot.config.update() + shutil.rmtree( + plugin.abs_path, + onerror=lambda *args: logger.warning('Failed to remove plugin files %s: %s', plugin, str(args[2])) + ) + try: + plugin.abs_path.parent.rmdir() + plugin.abs_path.parent.parent.rmdir() + except OSError: + pass # dir not empty embed = discord.Embed( description="The plugin is successfully uninstalled.", diff --git a/cogs/utility.py b/cogs/utility.py index d9fdde9837..2fa120b5b6 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -24,7 +24,6 @@ from core import checks from core.changelog import Changelog -from core.decorators import trigger_typing from core.models import InvalidConfigError, PermissionLevel from core.paginator import EmbedPaginatorSession, MessagePaginatorSession from core import utils @@ -248,7 +247,7 @@ def cog_unload(self): @commands.command() @checks.has_permissions(PermissionLevel.REGULAR) - @trigger_typing + @utils.trigger_typing async def changelog(self, ctx, version: str.lower = ""): """Shows the changelog of the Modmail.""" changelog = await Changelog.from_url(self.bot) @@ -282,7 +281,7 @@ async def changelog(self, ctx, version: str.lower = ""): @commands.command(aliases=["bot", "info"]) @checks.has_permissions(PermissionLevel.REGULAR) - @trigger_typing + @utils.trigger_typing async def about(self, ctx): """Shows information about this bot.""" embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) @@ -331,7 +330,7 @@ async def about(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.REGULAR) - @trigger_typing + @utils.trigger_typing async def sponsors(self, ctx): """Shows a list of sponsors.""" resp = await self.bot.session.get( @@ -352,7 +351,7 @@ async def sponsors(self, ctx): @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - @trigger_typing + @utils.trigger_typing async def debug(self, ctx): """Shows the recent application-logs of the bot.""" @@ -407,7 +406,7 @@ async def debug(self, ctx): @debug.command(name="hastebin", aliases=["haste"]) @checks.has_permissions(PermissionLevel.OWNER) - @trigger_typing + @utils.trigger_typing async def debug_hastebin(self, ctx): """Posts application-logs to Hastebin.""" @@ -450,7 +449,7 @@ async def debug_hastebin(self, ctx): @debug.command(name="clear", aliases=["wipe"]) @checks.has_permissions(PermissionLevel.OWNER) - @trigger_typing + @utils.trigger_typing async def debug_clear(self, ctx): """Clears the locally cached logs.""" @@ -658,7 +657,7 @@ async def before_loop_presence(self): @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - @trigger_typing + @utils.trigger_typing async def ping(self, ctx): """Pong! Returns your websocket latency.""" embed = discord.Embed( diff --git a/core/decorators.py b/core/decorators.py index b9e6f50067..c208af2adc 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -1,12 +1,10 @@ -import functools +import warnings -from discord.ext import commands +from core.utils import trigger_typing as _trigger_typing def trigger_typing(func): - @functools.wraps(func) - async def wrapper(self, ctx: commands.Context, *args, **kwargs): - await ctx.trigger_typing() - return await func(self, ctx, *args, **kwargs) - - return wrapper + warnings.warn("trigger_typing has been moved to core.utils.trigger_typing, this will be removed.", + DeprecationWarning, + stacklevel=2) + return _trigger_typing(func) diff --git a/core/utils.py b/core/utils.py index 146f6224af..ac50504ab5 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,3 +1,4 @@ +import functools import re import shlex import typing @@ -250,3 +251,12 @@ def format_description(i, names): ": ".join((str(a + i * 15), b)) for a, b in enumerate(takewhile(lambda x: x is not None, names), start=1) ) + + +def trigger_typing(func): + @functools.wraps(func) + async def wrapper(self, ctx: commands.Context, *args, **kwargs): + await ctx.trigger_typing() + return await func(self, ctx, *args, **kwargs) + + return wrapper From 9c77931e76897360804bcfffb29dfc962d0da6cf Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Sun, 29 Sep 2019 20:26:03 +0200 Subject: [PATCH 09/48] Add ?logs responded command (#338) --- cogs/modmail.py | 53 +++++++++++++++++++++++++++++++++++++++++---- core/_color_data.py | 6 +++-- core/models.py | 3 +-- core/thread.py | 8 ++++--- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index f8246e17e2..dcde629c20 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -17,7 +17,13 @@ from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed, format_description, strtobool +from core.utils import ( + format_preview, + User, + create_not_found_embed, + format_description, + strtobool, +) logger = logging.getLogger("Modmail") @@ -286,7 +292,9 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): @commands.command() @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() - async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None): + async def move( + self, ctx, category: discord.CategoryChannel, *, specifics: str = None + ): """ Move a thread to another category. @@ -297,7 +305,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = silent = False if specifics: - silent_words = ['silent', 'silently'] + silent_words = ["silent", "silently"] silent = any(word in silent_words for word in specifics.split()) await thread.channel.edit(category=category, sync_permissions=True) @@ -311,7 +319,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], - color=self.bot.main_color + color=self.bot.main_color, ) await thread.recipient.send(embed=embed) @@ -718,6 +726,43 @@ async def logs_closed_by(self, ctx, *, user: User = None): session = EmbedPaginatorSession(ctx, *embeds) await session.run() + @logs.command(name="responded") + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def logs_responded(self, ctx, *, user: User = None): + """ + Get all logs where the specified user has responded at least once. + + If no `user` is provided, the user will be the person who sent this command. + `user` may be a user ID, mention, or name. + """ + user = user if user is not None else ctx.author + + entries = [] + async for l in self.bot.db.logs.find( + { + "messages": { + "$elemMatch": { + "author.id": str(user.id), + "author.mod": True, + "type": {"$in": ["anonymous", "thread_message"]}, + } + } + } + ): + entries.append(l) + + embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) + + if not embeds: + embed = discord.Embed( + color=discord.Color.red(), + description="No log entries have been found for that query", + ) + return await ctx.send(embed=embed) + + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() + @logs.command(name="search", aliases=["find"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_search(self, ctx, limit: Optional[int] = None, *, query): diff --git a/core/_color_data.py b/core/_color_data.py index 997a6a1d25..b943645c4c 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -39,11 +39,13 @@ "light gray": "#979c9f", "dark gray": "#607d8b", "blurple": "#7289da", - "grayple": "#99aab5" + "grayple": "#99aab5", } # Normalize name to "discord:" to avoid name collisions. -DISCORD_COLORS_NORM = {"discord:" + name: value for name, value in DISCORD_COLORS.items()} +DISCORD_COLORS_NORM = { + "discord:" + name: value for name, value in DISCORD_COLORS.items() +} # These colors are from Tableau diff --git a/core/models.py b/core/models.py index 8b21c10eb0..b7cd72906a 100644 --- a/core/models.py +++ b/core/models.py @@ -90,7 +90,6 @@ class _Default: class SafeFormatter(Formatter): - def get_field(self, field_name, args, kwargs): first, rest = _string.formatter_field_name_split(field_name) @@ -107,7 +106,7 @@ def get_field(self, field_name, args, kwargs): if n >= 2: break if is_attr: - if str(i).startswith('_'): + if str(i).startswith("_"): break obj = getattr(obj, i) else: diff --git a/core/thread.py b/core/thread.py index bdf0bc7aa2..33d38410ee 100644 --- a/core/thread.py +++ b/core/thread.py @@ -394,7 +394,10 @@ async def _close( message = self.bot.config["thread_close_response"] message = self.bot.formatter.format( - message, closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None + message, + closer=closer, + loglink=log_url, + logkey=log_data["key"] if log_data else None, ) embed.description = message @@ -493,8 +496,7 @@ async def _restart_close_timer(self): # Grab message close_message = self.bot.formatter.format( - self.bot.config["thread_auto_close_response"], - timeout=human_time + self.bot.config["thread_auto_close_response"], timeout=human_time ) time_marker_regex = "%t" From 31375986694b56ed4cb010e638ae0eff4c4be1b6 Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Wed, 2 Oct 2019 20:56:57 +0200 Subject: [PATCH 10/48] Add anon_reply_without_command --- bot.py | 11 ++++++++++- core/config.py | 2 ++ core/config_help.json | 13 ++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index a25777160d..c3a65f2d51 100644 --- a/bot.py +++ b/bot.py @@ -867,7 +867,16 @@ async def process_commands(self, message): except ValueError: reply_without_command = self.config.remove("reply_without_command") - if reply_without_command: + try: + anon_reply_without_command = strtobool( + self.config["anon_reply_without_command"] + ) + except ValueError: + anon_reply_without_command = self.config.remove("anon_reply_without_command") + + if anon_reply_without_command: + await thread.reply(message, anonymous=True) + elif reply_without_command: await thread.reply(message) else: await self.api.append_log(message, type_="internal") diff --git a/core/config.py b/core/config.py index ed9adead23..f3dedbfe1a 100644 --- a/core/config.py +++ b/core/config.py @@ -36,6 +36,7 @@ class ConfigManager: "account_age": None, "guild_age": None, "reply_without_command": False, + "anon_reply_without_command": False, # logging "log_channel_id": None, # threads @@ -110,6 +111,7 @@ class ConfigManager: "user_typing", "mod_typing", "reply_without_command", + "anon_reply_without_command", "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", diff --git a/core/config_help.json b/core/config_help.json index 50d0a9fad9..89bff67bdb 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -111,7 +111,18 @@ "`{prefix}config set reply_without_command no`" ], "notes": [ - "Unfortunately, anonymous `reply_without_command` is currently not possible." + "See also: `anon_reply_without_command`." + ] + }, + "anon_reply_without_command": { + "default": "Disabled", + "description": "Setting this configuration will make all non-command messages sent in the thread channel to be anonymously forwarded to the recipient without the need of `{prefix}reply`.", + "examples": [ + "`{prefix}config set anon_reply_without_command yes`", + "`{prefix}config set anon_reply_without_command no`" + ], + "notes": [ + "See also: `reply_without_command`." ] }, "log_channel_id": { From 1201f21e3221fc31024ded1ca293928db9bd232e Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Fri, 4 Oct 2019 09:12:03 +0200 Subject: [PATCH 11/48] Moved db query to core/client;py --- bot.py | 4 +++- cogs/modmail.py | 14 +------------- core/clients.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/bot.py b/bot.py index c3a65f2d51..3abb0aae72 100644 --- a/bot.py +++ b/bot.py @@ -872,7 +872,9 @@ async def process_commands(self, message): self.config["anon_reply_without_command"] ) except ValueError: - anon_reply_without_command = self.config.remove("anon_reply_without_command") + anon_reply_without_command = self.config.remove( + "anon_reply_without_command" + ) if anon_reply_without_command: await thread.reply(message, anonymous=True) diff --git a/cogs/modmail.py b/cogs/modmail.py index dcde629c20..3fa5062b15 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -737,19 +737,7 @@ async def logs_responded(self, ctx, *, user: User = None): """ user = user if user is not None else ctx.author - entries = [] - async for l in self.bot.db.logs.find( - { - "messages": { - "$elemMatch": { - "author.id": str(user.id), - "author.mod": True, - "type": {"$in": ["anonymous", "thread_message"]}, - } - } - } - ): - entries.append(l) + entries = await self.bot.api.get_responded_logs(user.id) embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) diff --git a/core/clients.py b/core/clients.py index a8bb006e6d..edfe928cd8 100644 --- a/core/clients.py +++ b/core/clients.py @@ -92,6 +92,23 @@ async def get_user_logs(self, user_id: Union[str, int]) -> list: return await self.logs.find(query, projection).to_list(None) + async def get_responded_logs(self, user_id: Union[str, int]) -> list: + entries = [] + async for l in self.bot.db.logs.find( + { + "open": False, + "messages": { + "$elemMatch": { + "author.id": str(user_id), + "author.mod": True, + "type": {"$in": ["anonymous", "thread_message"]}, + } + } + } + ): + entries.append(l) + return entries + async def get_log(self, channel_id: Union[str, int]) -> dict: logger.debug("Retrieving channel %s logs.", channel_id) return await self.logs.find_one({"channel_id": str(channel_id)}) From bc8abd819ef32968bcf84797512f3a3c5e6f1f74 Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Fri, 4 Oct 2019 16:55:29 +0200 Subject: [PATCH 12/48] Fix regex for scheduled closes --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 3abb0aae72..8b176a089b 100644 --- a/bot.py +++ b/bot.py @@ -683,7 +683,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: self.blocked_users.pop(str(message.author.id)) else: reaction = blocked_emoji - end_time = re.search(r"%(.+?)%$", reason) + end_time = re.search(r"%(.+?)%", reason) if end_time is not None: logger.debug("No longer blocked, user %s.", message.author.name) after = ( From c399eff9f25842c30a2cbef4a2668d9d853c479a Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Fri, 4 Oct 2019 16:59:22 +0200 Subject: [PATCH 13/48] Add a comma uwu --- core/clients.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/clients.py b/core/clients.py index edfe928cd8..71525892ef 100644 --- a/core/clients.py +++ b/core/clients.py @@ -103,7 +103,7 @@ async def get_responded_logs(self, user_id: Union[str, int]) -> list: "author.mod": True, "type": {"$in": ["anonymous", "thread_message"]}, } - } + }, } ): entries.append(l) From 3881d228603840c729647ca09488c68e181d7a10 Mon Sep 17 00:00:00 2001 From: Robin Mahieu <42642013+papiersnipper@users.noreply.github.com> Date: Sun, 6 Oct 2019 10:40:38 +0200 Subject: [PATCH 14/48] Oopsie this line shouldn't be here --- cogs/modmail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 1578016a02..7e19d47768 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -18,7 +18,6 @@ from core.time import UserFriendlyTime, human_timedelta from core.utils import format_preview, User, create_not_found_embed, format_description, trigger_typing - logger = logging.getLogger("Modmail") From 5ecde89520d9457d3a8937bfe5a14925e912624f Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sun, 6 Oct 2019 08:17:50 -0400 Subject: [PATCH 15/48] Update mention --- core/config_help.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config_help.json b/core/config_help.json index c24b5868fa..b9f0ad8204 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -39,7 +39,7 @@ "`{prefix}mention Yo~ Here's a new thread for ya!`" ], "notes": [ - "Unfortunately, its not currently possible to disable mention. You do not have to include a mention." + "Unfortunately, it's not currently possible to disable mention. You do not have to include a mention." ] }, "main_color": { From 0c4375462d52e60f64e22fefbd0c0587d2438551 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 6 Oct 2019 19:54:02 -0700 Subject: [PATCH 16/48] Ability to update all plugins --- CHANGELOG.md | 1 + cogs/plugins.py | 32 +++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0edfa116..d6a5189d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). - Help command is in alphabetical order grouped by permissions. - Notes are no longer always blurple, its set to `MAIN_COLOR` now. +- Added `?plugins update` for updating all installed plugins. ### Internal diff --git a/cogs/plugins.py b/cogs/plugins.py index 3adbd8c3bb..1e1c07c761 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -413,16 +413,8 @@ async def plugins_remove(self, ctx, *, plugin_name: str): ) await ctx.send(embed=embed) - @plugins.command(name="update") - @checks.has_permissions(PermissionLevel.OWNER) - async def plugins_update(self, ctx, *, plugin_name: str): - """ - Update a plugin for the bot. - - `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference - to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). - """ - + async def update_plugin(self, ctx, plugin_name): + logger.debug("Updating %s.", plugin_name) plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) if plugin is None: return @@ -441,13 +433,31 @@ async def plugins_update(self, ctx, *, plugin_name: str): except commands.ExtensionError: logger.warning("Plugin unload fail.", exc_info=True) await self.load_plugin(plugin) - + logger.debug("Updated %s.", plugin_name) embed = discord.Embed( description=f"Successfully updated {plugin.name}.", color=self.bot.main_color, ) return await ctx.send(embed=embed) + @plugins.command(name="update") + @checks.has_permissions(PermissionLevel.OWNER) + async def plugins_update(self, ctx, *, plugin_name: str = None): + """ + Update a plugin for the bot. + + `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + + To update all plugins, do `{prefix}plugins update`. + """ + + if plugin_name is None: + for plugin_name in self.bot.config["plugins"]: + await self.update_plugin(ctx, plugin_name) + else: + await self.update_plugin(ctx, plugin_name) + @plugins.command(name="loaded", aliases=["enabled", "installed"]) @checks.has_permissions(PermissionLevel.OWNER) async def plugins_loaded(self, ctx): From 516709e83e8f6ed73019a84a3cb54eaf441dd15e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 6 Oct 2019 20:36:40 -0700 Subject: [PATCH 17/48] Update README --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 71b17e8e30..e8f9d97bbf 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,12 @@ Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins -Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). +Modmail supports the use of third-party plugins to extend or add functionalities to the bot. +This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. + +A list of third-party plugins can be found using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. + +To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). Plugins requests and support is available in our [Modmail Plugins Server](https://discord.gg/4JE4XSW). From 82da38a793083bfcd9c7a9c1536de235c3ca4d84 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 02:01:04 -0700 Subject: [PATCH 18/48] Update travis --- .bandit_baseline.json | 243 ++++++++++++++++++++++++++++++++++++++++++ .travis.yml | 19 +++- CHANGELOG.md | 2 + Pipfile | 2 + Pipfile.lock | 134 +++++++++++++++++++---- cogs/utility.py | 11 +- core/thread.py | 8 +- core/utils.py | 7 -- 8 files changed, 387 insertions(+), 39 deletions(-) create mode 100644 .bandit_baseline.json diff --git a/.bandit_baseline.json b/.bandit_baseline.json new file mode 100644 index 0000000000..539fe85a88 --- /dev/null +++ b/.bandit_baseline.json @@ -0,0 +1,243 @@ +{ + "errors": [], + "generated_at": "2019-10-07T08:19:22Z", + "metrics": { + "./bot.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 933, + "nosec": 0 + }, + "_totals": { + "CONFIDENCE.HIGH": 2.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 1.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 2.0, + "SEVERITY.MEDIUM": 1.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 7299, + "nosec": 0 + }, + "cogs/modmail.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 973, + "nosec": 0 + }, + "cogs/plugins.py": { + "CONFIDENCE.HIGH": 1.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 1.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 537, + "nosec": 0 + }, + "cogs/utility.py": { + "CONFIDENCE.HIGH": 1.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 1.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 1587, + "nosec": 0 + }, + "core/_color_data.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 1168, + "nosec": 0 + }, + "core/changelog.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 154, + "nosec": 0 + }, + "core/checks.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 75, + "nosec": 0 + }, + "core/clients.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 200, + "nosec": 0 + }, + "core/config.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 276, + "nosec": 0 + }, + "core/decorators.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 7, + "nosec": 0 + }, + "core/models.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 91, + "nosec": 0 + }, + "core/paginator.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 214, + "nosec": 0 + }, + "core/thread.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 716, + "nosec": 0 + }, + "core/time.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 169, + "nosec": 0 + }, + "core/utils.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 1.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 1.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 199, + "nosec": 0 + } + }, + "results": [ + { + "code": "14 from site import USER_SITE\n15 from subprocess import PIPE\n16 \n17 import discord\n", + "filename": "cogs/plugins.py", + "issue_confidence": "HIGH", + "issue_severity": "LOW", + "issue_text": "Consider possible security implications associated with PIPE module.", + "line_number": 15, + "line_range": [ + 15, + 16 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", + "test_id": "B404", + "test_name": "blacklist" + }, + { + "code": "1824 try:\n1825 exec(to_compile, env) # pylint: disable=exec-used\n1826 except Exception as exc:\n", + "filename": "cogs/utility.py", + "issue_confidence": "HIGH", + "issue_severity": "MEDIUM", + "issue_text": "Use of exec detected.", + "line_number": 1825, + "line_range": [ + 1825 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b102_exec_used.html", + "test_id": "B102", + "test_name": "exec_used" + }, + { + "code": "219 for token in shlex.shlex(alias, punctuation_chars=\"&\"):\n220 if token != \"&&\":\n221 buffer += \" \" + token\n", + "filename": "core/utils.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: '&&'", + "line_number": 220, + "line_range": [ + 220 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html", + "test_id": "B105", + "test_name": "hardcoded_password_string" + } + ] +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 14f9928d1b..65654b83fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,27 @@ language: python matrix: include: - python: '3.7' - dist: xenial + name: "Python 3.7.1 on Xenial Linux" + - python: '3.6' + name: "Python 3.6.7 on Xenial Linux" + - name: "Python 3.7.4 on macOS" + os: osx + osx_image: xcode11 + language: shell + - name: "Python 3.7.4 on Windows" + os: windows + language: shell + before_install: + - choco install python + - python -m pip install --upgrade pip + env: PATH=/c/Python37:/c/Python37/Scripts:$PATH install: + - pip3 install --upgrade pip + - pip3 install pipenv - pipenv install -d script: + - pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - pipenv run python .lint.py + - pipenv run flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 --exit-zero diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a5189d56..a50704deff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Help command is in alphabetical order grouped by permissions. - Notes are no longer always blurple, its set to `MAIN_COLOR` now. - Added `?plugins update` for updating all installed plugins. +- Reintroduce flake8 and use bandit for security issues detection. +- Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. ### Internal diff --git a/Pipfile b/Pipfile index cfb4f8ba59..7b54ff37b2 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,8 @@ verify_ssl = true [dev-packages] black = "==19.3b0" pylint = "*" +bandit = "==1.6.2" +flake8 = "==3.7.8" [packages] colorama = ">=0.4.0" diff --git a/Pipfile.lock b/Pipfile.lock index ee4681eb19..431469cdcf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "564773a120ff7ba8214e5efd98fab8daf98230eb4b27f8af2dd63afcad39994c" + "sha256": "f5ceea517895626871c9bb2c559e39f6b81859f55669897aa9533154a4df1431" }, "pipfile-spec": 6, "requires": { @@ -53,17 +53,17 @@ }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", + "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" ], - "version": "==19.1.0" + "version": "==19.2.0" }, "certifi": { "hashes": [ - "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", - "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", + "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" ], - "version": "==2019.6.16" + "version": "==2019.9.11" }, "chardet": { "hashes": [ @@ -97,10 +97,10 @@ }, "emoji": { "hashes": [ - "sha256:b68112d40482a05e5da5d53da33d0aba3cce96891282c9c179cc340003c6c64e" + "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174" ], "index": "pypi", - "version": "==0.5.3" + "version": "==0.5.4" }, "future": { "hashes": [ @@ -261,10 +261,10 @@ }, "virtualenv": { "hashes": [ - "sha256:94a6898293d07f84a98add34c4df900f8ec64a570292279f6d91c781d37fd305", - "sha256:f6fc312c031f2d2344f885de114f1cb029dfcffd26aa6e57d2ee2296935c4e7d" + "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", + "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" ], - "version": "==16.7.4" + "version": "==16.7.5" }, "virtualenv-clone": { "hashes": [ @@ -326,17 +326,25 @@ }, "astroid": { "hashes": [ - "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", - "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" + "sha256:98c665ad84d10b18318c5ab7c3d203fe11714cbad2a4aef4f44651f415392754", + "sha256:b7546ffdedbf7abcfbff93cd1de9e9980b1ef744852689decc5aeada324238c6" ], - "version": "==2.2.5" + "version": "==2.3.1" }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", + "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" ], - "version": "==19.1.0" + "version": "==19.2.0" + }, + "bandit": { + "hashes": [ + "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", + "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065" + ], + "index": "pypi", + "version": "==1.6.2" }, "black": { "hashes": [ @@ -353,6 +361,35 @@ ], "version": "==7.0" }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "flake8": { + "hashes": [ + "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", + "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" + ], + "index": "pypi", + "version": "==3.7.8" + }, + "gitdb2": { + "hashes": [ + "sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", + "sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b" + ], + "version": "==2.0.6" + }, + "gitpython": { + "hashes": [ + "sha256:631263cc670aa56ce3d3c414cf0fe2e840f2e913514b138ea28d88a477bbcd21", + "sha256:6e97b9f0954807f30c2dd8e3165731ed6c477a1b365f194b69d81d7940a08332" + ], + "version": "==3.0.3" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -390,13 +427,52 @@ ], "version": "==0.6.1" }, + "pbr": { + "hashes": [ + "sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", + "sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9" + ], + "version": "==5.4.3" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, "pylint": { "hashes": [ - "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", - "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" + "sha256:7edbae11476c2182708063ac387a8f97c760d9cfe36a5ede0ca996f90cf346c8", + "sha256:844ce067788028c1a35086a5c66bc5e599ddd851841c41d6ee1623b36774d9f2" ], "index": "pypi", - "version": "==2.3.1" + "version": "==2.4.2" + }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "version": "==5.1.2" }, "six": { "hashes": [ @@ -405,6 +481,20 @@ ], "version": "==1.12.0" }, + "smmap2": { + "hashes": [ + "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", + "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a" + ], + "version": "==2.0.5" + }, + "stevedore": { + "hashes": [ + "sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", + "sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14" + ], + "version": "==1.31.0" + }, "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", @@ -430,7 +520,7 @@ "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" ], - "markers": "implementation_name == 'cpython'", + "markers": "implementation_name == 'cpython' and python_version < '3.8'", "version": "==1.4.0" }, "wrapt": { diff --git a/cogs/utility.py b/cogs/utility.py index 2fa120b5b6..da95df297b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -272,12 +272,11 @@ async def changelog(self, ctx, version: str.lower = ""): except Exception: try: await paginator.close() - except Exception: - pass - logger.warning("Failed to display changelog.", exc_info=True) - await ctx.send( - f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" - ) + finally: + logger.warning("Failed to display changelog.", exc_info=True) + await ctx.send( + f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" + ) @commands.command(aliases=["bot", "info"]) @checks.has_permissions(PermissionLevel.REGULAR) diff --git a/core/thread.py b/core/thread.py index ce25e49652..bb53eed455 100644 --- a/core/thread.py +++ b/core/thread.py @@ -12,7 +12,7 @@ from discord.ext.commands import MissingRequiredArgument, CommandError from core.time import human_timedelta -from core.utils import is_image_url, days, match_user_id, truncate, ignore +from core.utils import is_image_url, days, match_user_id, truncate logger = logging.getLogger("Modmail") @@ -761,8 +761,10 @@ async def send( self.ready = True if delete_message: - self.bot.loop.create_task(ignore(message.delete())) - + try: + await message.delete() + except discord.HTTPException: + logger.warning('Cannot delete message.', exc_info=True) return msg def get_notifications(self) -> str: diff --git a/core/utils.py b/core/utils.py index ac50504ab5..fd85f0d5b0 100644 --- a/core/utils.py +++ b/core/utils.py @@ -195,13 +195,6 @@ def match_user_id(text: str) -> int: return -1 -async def ignore(coro): - try: - await coro - except Exception: - pass - - def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discord.Embed: # Single reference of Color.red() embed = discord.Embed( From 06c8f783ea00eee0b5e2ae8ec7fa25ed75ece5b7 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 02:17:35 -0700 Subject: [PATCH 19/48] add main check --- .lint.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.lint.py b/.lint.py index 1cd6feba43..fd3cbc2f19 100644 --- a/.lint.py +++ b/.lint.py @@ -1,16 +1,17 @@ -import sys -from os import listdir -from os.path import join +if __name__ == '__main__': + import sys + from os import listdir + from os.path import join -from pylint.lint import Run + from pylint.lint import Run -THRESHOLD = 9.75 + THRESHOLD = 9.75 -cogs = [join("cogs", c) for c in listdir("cogs") if c.endswith(".py")] -core = [join("core", c) for c in listdir("core") if c.endswith(".py")] + cogs = [join("cogs", c) for c in listdir("cogs") if c.endswith(".py")] + core = [join("core", c) for c in listdir("core") if c.endswith(".py")] -results = Run(["bot.py", *cogs, *core], do_exit=False) + results = Run(["bot.py", *cogs, *core], do_exit=False) -score = results.linter.stats["global_note"] -if score <= THRESHOLD: - sys.exit(1) + score = results.linter.stats["global_note"] + if score <= THRESHOLD: + sys.exit(1) From 7a24b64654d7deb7b0c2aa3232bd31649eb08954 Mon Sep 17 00:00:00 2001 From: Robin Mahieu <42642013+papiersnipper@users.noreply.github.com> Date: Mon, 7 Oct 2019 18:46:13 +0200 Subject: [PATCH 20/48] Make error message more personal Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7e19d47768..fa634b6108 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -722,7 +722,7 @@ async def logs_responded(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( color=discord.Color.red(), - description="No log entries have been found for that query", + description=f"{getattr(user, 'mention', user.id)} has not responded to any threads.", ) return await ctx.send(embed=embed) From 31c2c072c9a3fbb08d312a0e531ae69f2af76ac1 Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Mon, 7 Oct 2019 18:56:21 +0200 Subject: [PATCH 21/48] Switch to the new features in the current dev branch --- bot.py | 20 ++------------------ cogs/modmail.py | 2 +- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/bot.py b/bot.py index 36cf8f3cd6..170afe47ce 100644 --- a/bot.py +++ b/bot.py @@ -829,25 +829,9 @@ async def process_commands(self, message): thread = await self.threads.find(channel=ctx.channel) if thread is not None: - try: - reply_without_command = strtobool( - self.config["reply_without_command"] - ) - except ValueError: - reply_without_command = self.config.remove("reply_without_command") - - try: - anon_reply_without_command = strtobool( - self.config["anon_reply_without_command"] - ) - except ValueError: - anon_reply_without_command = self.config.remove( - "anon_reply_without_command" - ) - - if anon_reply_without_command: + if self.config.get("anon_reply_without_command"): await thread.reply(message, anonymous=True) - elif reply_without_command: + elif self.config.get("reply_without_command"): await thread.reply(message) else: await self.api.append_log(message, type_="internal") diff --git a/cogs/modmail.py b/cogs/modmail.py index fa634b6108..9e11a26a3f 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -721,7 +721,7 @@ async def logs_responded(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{getattr(user, 'mention', user.id)} has not responded to any threads.", ) return await ctx.send(embed=embed) From ed8ee39f6e2f25f475156350a2a8b3f51c71ade7 Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Mon, 7 Oct 2019 18:59:17 +0200 Subject: [PATCH 22/48] I heard we're still using balck --- cogs/modmail.py | 8 +++++++- cogs/plugins.py | 4 +++- core/decorators.py | 8 +++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 9e11a26a3f..72f4f9f8ed 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -16,7 +16,13 @@ from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed, format_description, trigger_typing +from core.utils import ( + format_preview, + User, + create_not_found_embed, + format_description, + trigger_typing, +) logger = logging.getLogger("Modmail") diff --git a/cogs/plugins.py b/cogs/plugins.py index 3adbd8c3bb..5a36f66d31 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -399,7 +399,9 @@ async def plugins_remove(self, ctx, *, plugin_name: str): await self.bot.config.update() shutil.rmtree( plugin.abs_path, - onerror=lambda *args: logger.warning('Failed to remove plugin files %s: %s', plugin, str(args[2])) + onerror=lambda *args: logger.warning( + "Failed to remove plugin files %s: %s", plugin, str(args[2]) + ), ) try: plugin.abs_path.parent.rmdir() diff --git a/core/decorators.py b/core/decorators.py index c208af2adc..0107a6b2d6 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -4,7 +4,9 @@ def trigger_typing(func): - warnings.warn("trigger_typing has been moved to core.utils.trigger_typing, this will be removed.", - DeprecationWarning, - stacklevel=2) + warnings.warn( + "trigger_typing has been moved to core.utils.trigger_typing, this will be removed.", + DeprecationWarning, + stacklevel=2, + ) return _trigger_typing(func) From 4050d31dbc4b3910eefa3fb2e61688e399fd0122 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 12:42:26 -0700 Subject: [PATCH 23/48] Updated changelog 3.3.0-dev2 --- CHANGELOG.md | 15 +++++++++++---- bot.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a50704deff..1bbf974230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,20 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev1 +# v3.3.0-dev2 ### Added -- Two new config vars: - - `ENABLE_PLUGINS` (yes/no default yes), when set to no, plugins will not be loaded into the bot. - - `ERROR_COLOR` (color format, defaults discord red), the color of error messages. +- Three new config vars: + - `enable_plugins` (yes/no default yes) + - When set to no, plugins will not be loaded into the bot. + - `error_color` (color format, defaults discord red) + - The color of error messages. + - `anon_reply_without_command` (yes/no default no) (Thanks to papiersnipper PR#288) + - When set, all non-command messages sent to thread channels are forwarded to the recipient anonymously without the need of `?anonreply`. + - This config takes precedence over `reply_without_command`. +- `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) + - `user` when not provided, defaults to the user who ran the command. ### Changed diff --git a/bot.py b/bot.py index 170afe47ce..212b8041de 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev1" +__version__ = "3.3.0-dev2" import asyncio import logging From 1326e5d65b267fe8ca293d0c79529de4ce7491b5 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 13:31:37 -0700 Subject: [PATCH 24/48] Auto close limbo open threads --- CHANGELOG.md | 1 + bot.py | 23 +++++++++++++++++++++++ core/clients.py | 32 +++++++++++++++++--------------- core/thread.py | 5 +++-- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbf974230..ffe4c28b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - This config takes precedence over `reply_without_command`. - `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) - `user` when not provided, defaults to the user who ran the command. +- Open threads in limbo now auto closes if the channel cannot be found. This check is done every time the bot restarts. ### Changed diff --git a/bot.py b/bot.py index 212b8041de..ec9bc4c857 100644 --- a/bot.py +++ b/bot.py @@ -502,6 +502,29 @@ async def on_ready(self): auto_close=items.get("auto_close", False), ) + for log in await self.api.get_open_logs(): + if self.get_channel(int(log['channel_id'])) is None: + logger.debug("Unable to resolve thread with channel %s.", log['channel_id']) + log_data = await self.api.post_log( + log['channel_id'], + { + "open": False, + "closed_at": str(datetime.utcnow()), + "close_message": "Channel has been deleted, no closer found.", + "closer": { + "id": str(self.user.id), + "name": self.user.name, + "discriminator": self.user.discriminator, + "avatar_url": str(self.user.avatar_url), + "mod": True, + }, + }, + ) + if log_data: + logger.debug("Successfully closed thread with channel %s.", log['channel_id']) + else: + logger.debug("Failed to close thread with channel %s, skipping.", log['channel_id']) + self.metadata_loop = tasks.Loop( self.post_metadata, seconds=0, diff --git a/core/clients.py b/core/clients.py index 71525892ef..f01753bf23 100644 --- a/core/clients.py +++ b/core/clients.py @@ -93,21 +93,23 @@ async def get_user_logs(self, user_id: Union[str, int]) -> list: return await self.logs.find(query, projection).to_list(None) async def get_responded_logs(self, user_id: Union[str, int]) -> list: - entries = [] - async for l in self.bot.db.logs.find( - { - "open": False, - "messages": { - "$elemMatch": { - "author.id": str(user_id), - "author.mod": True, - "type": {"$in": ["anonymous", "thread_message"]}, - } - }, - } - ): - entries.append(l) - return entries + query = { + "open": False, + "messages": { + "$elemMatch": { + "author.id": str(user_id), + "author.mod": True, + "type": {"$in": ["anonymous", "thread_message"]}, + } + }, + } + return await self.logs.find(query).to_list(None) + + async def get_open_logs(self) -> list: + query = { + "open": True + } + return await self.logs.find(query).to_list(None) async def get_log(self, channel_id: Union[str, int]) -> dict: logger.debug("Retrieving channel %s logs.", channel_id) diff --git a/core/thread.py b/core/thread.py index bb53eed455..207fdae97d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -762,8 +762,9 @@ async def send( if delete_message: try: - await message.delete() - except discord.HTTPException: + if isinstance(message.channel, discord.TextChannel): + await message.delete() + except Exception: logger.warning('Cannot delete message.', exc_info=True) return msg From 25162b7efb63d2fd7ce53146a0423d5b2c4b8c15 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 13:37:07 -0700 Subject: [PATCH 25/48] Add closer check --- cogs/modmail.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 72f4f9f8ed..8701ac6bd6 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -626,7 +626,12 @@ def format_log_embeds(self, logs, avatar_url): embed.add_field( name="Created", value=duration(created_at, now=datetime.utcnow()) ) - embed.add_field(name="Closed By", value=f"<@{entry['closer']['id']}>") + closer = entry.get('closer') + if closer is None: + closer = 'Unknown' + else: + closer = f"<@{closer['id']}>" + embed.add_field(name="Closed By", value=closer) if entry["recipient"]["id"] != entry["creator"]["id"]: embed.add_field(name="Created by", value=f"<@{entry['creator']['id']}>") From df0853f6ba98dfa9258f1c8b2c6ec783b54edc13 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 13:42:58 -0700 Subject: [PATCH 26/48] No closer -> no log url --- cogs/modmail.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 8701ac6bd6..9ebc36d8fd 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -628,10 +628,10 @@ def format_log_embeds(self, logs, avatar_url): ) closer = entry.get('closer') if closer is None: - closer = 'Unknown' + closer_msg = 'Unknown' else: - closer = f"<@{closer['id']}>" - embed.add_field(name="Closed By", value=closer) + closer_msg = f"<@{closer['id']}>" + embed.add_field(name="Closed By", value=closer_msg) if entry["recipient"]["id"] != entry["creator"]["id"]: embed.add_field(name="Created by", value=f"<@{entry['creator']['id']}>") @@ -639,7 +639,13 @@ def format_log_embeds(self, logs, avatar_url): embed.add_field( name="Preview", value=format_preview(entry["messages"]), inline=False ) - embed.add_field(name="Link", value=log_url) + + if closer is not None: + # BUG: Currently, logviewer can't display logs without a closer. + embed.add_field(name="Link", value=log_url) + else: + embed.add_field(name="Log Key", value=entry['key']) + embed.set_footer(text="Recipient ID: " + str(entry["recipient"]["id"])) embeds.append(embed) return embeds From 8e01aaafbfd2045c5abf5e62a4574dfe7a662e13 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 13:49:10 -0700 Subject: [PATCH 27/48] Formatting --- cogs/modmail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 9ebc36d8fd..67e840bded 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -644,7 +644,8 @@ def format_log_embeds(self, logs, avatar_url): # BUG: Currently, logviewer can't display logs without a closer. embed.add_field(name="Link", value=log_url) else: - embed.add_field(name="Log Key", value=entry['key']) + logger.debug("Invalid log entry: no closer.") + embed.add_field(name="Log Key", value=f"`{entry['key']}`") embed.set_footer(text="Recipient ID: " + str(entry["recipient"]["id"])) embeds.append(embed) From 1267b8181bf42858df3575e4fe26f232f8942c07 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 9 Oct 2019 01:43:53 -0700 Subject: [PATCH 28/48] Improved internal activity logic --- CHANGELOG.md | 3 + bot.py | 2 +- cogs/utility.py | 170 ++++++++++++++++++++---------------------------- core/config.py | 25 +++++++ core/thread.py | 14 ++-- 5 files changed, 106 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe4c28b52..add6bb5d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Added `?plugins update` for updating all installed plugins. - Reintroduce flake8 and use bandit for security issues detection. - Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. +- Eval commands are logged in debug logs. +- Presence updates 30 minutes instead of 45 now. ### Internal @@ -44,6 +46,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - To get/set the raw value, access through `config[]`. - Prerelease naming scheme is now `x.x.x-devN`. - `trigger_typing` has been moved to `core.utils.trigger_typing`, original location is deprecated. +- Simpler status and activity logic. # v3.2.2 diff --git a/bot.py b/bot.py index ec9bc4c857..07a4bc5096 100644 --- a/bot.py +++ b/bot.py @@ -444,7 +444,7 @@ async def setup_indexes(self): ("key", "text"), ] ) - logger.debug("Successfully set up database indexes.") + logger.debug("Successfully configured and verified database indexes.") async def on_ready(self): """Bot startup, sets uptime.""" diff --git a/cogs/utility.py b/cogs/utility.py index da95df297b..d8dd9f97a5 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -501,20 +501,23 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): if not message: raise commands.MissingRequiredArgument(SimpleNamespace(name="message")) - activity, msg = ( - await self.set_presence( - activity_identifier=activity_type, - activity_by_key=True, - activity_message=message, - ) - )["activity"] - if activity is None: + try: + activity_type = ActivityType[activity_type] + except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) + activity, _ = await self.set_presence(activity_type=activity_type, activity_message=message) + self.bot.config["activity_type"] = activity.type.value - self.bot.config["activity_message"] = message + self.bot.config["activity_message"] = activity.name await self.bot.config.update() + msg = f"Activity set to: {activity.type.name.capitalize()} " + if activity.type == ActivityType.listening: + msg += f"to {activity.name}." + else: + msg += f"{activity.name}." + embed = discord.Embed( title="Activity Changed", description=msg, color=self.bot.main_color ) @@ -529,8 +532,7 @@ async def status(self, ctx, *, status_type: str.lower): Possible status types: - `online` - `idle` - - `dnd` - - `do_not_disturb` or `do not disturb` + - `dnd` or `do not disturb` - `invisible` or `offline` To remove the current status: @@ -542,117 +544,85 @@ async def status(self, ctx, *, status_type: str.lower): await self.set_presence() embed = discord.Embed(title="Status Removed", color=self.bot.main_color) return await ctx.send(embed=embed) - status_type = status_type.replace(" ", "_") - status, msg = ( - await self.set_presence(status_identifier=status_type, status_by_key=True) - )["status"] - if status is None: + status_type = status_type.replace(" ", "_") + try: + status = Status[status_type] + except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="status")) + _, status = await self.set_presence(status=status) + self.bot.config["status"] = status.value await self.bot.config.update() + msg = f"Status set to: {status.value}." embed = discord.Embed( title="Status Changed", description=msg, color=self.bot.main_color ) return await ctx.send(embed=embed) - async def set_presence( - self, - *, - status_identifier=None, - status_by_key=True, - activity_identifier=None, - activity_by_key=True, - activity_message=None, - ): - - activity = status = None - if status_identifier is None: - status_identifier = self.bot.config["status"] - status_by_key = False + async def set_presence(self, *, status=None, activity_type=None, activity_message=None): - try: - if status_by_key: - status = Status[status_identifier] - else: - status = Status.try_value(status_identifier) - except (KeyError, ValueError): - if status_identifier is not None: - msg = f"Invalid status type: {status_identifier}" - logger.warning(msg) - - if activity_identifier is None: - if activity_message is not None: - raise ValueError( - "activity_message must be None if activity_identifier is None." - ) - activity_identifier = self.bot.config["activity_type"] - activity_by_key = False - - try: - if activity_by_key: - activity_type = ActivityType[activity_identifier] - else: - activity_type = ActivityType.try_value(activity_identifier) - except (KeyError, ValueError): - if activity_identifier is not None: - msg = f"Invalid activity type: {activity_identifier}" - logger.warning(msg) + if status is None: + status = self.bot.config.get("status") + + if activity_type is None: + activity_type = self.bot.config.get("activity_type") + + url = None + activity_message = (activity_message or self.bot.config["activity_message"]).strip() + if activity_type is not None and not activity_message: + logger.warning("No activity message found whilst activity is provided, defaults to \"Modmail\".") + activity_message = "Modmail" + + if activity_type == ActivityType.listening: + if activity_message.lower().startswith("to "): + # The actual message is after listening to [...] + # discord automatically add the "to" + activity_message = activity_message[3:].strip() + elif activity_type == ActivityType.streaming: + url = self.bot.config["twitch_url"] + + if activity_type is not None: + activity = discord.Activity( + type=activity_type, name=activity_message, url=url + ) else: - url = None - activity_message = ( - activity_message or self.bot.config["activity_message"] - ).strip() - - if activity_type == ActivityType.listening: - if activity_message.lower().startswith("to "): - # The actual message is after listening to [...] - # discord automatically add the "to" - activity_message = activity_message[3:].strip() - elif activity_type == ActivityType.streaming: - url = self.bot.config["twitch_url"] - - if activity_message: - activity = discord.Activity( - type=activity_type, name=activity_message, url=url - ) - else: - msg = "You must supply an activity message to use custom activity." - logger.debug(msg) - + activity = None await self.bot.change_presence(activity=activity, status=status) - presence = { - "activity": (None, "No activity has been set."), - "status": (None, "No status has been set."), - } - if activity is not None: - use_to = "to " if activity.type == ActivityType.listening else "" - msg = f"Activity set to: {activity.type.name.capitalize()} " - msg += f"{use_to}{activity.name}." - presence["activity"] = (activity, msg) - if status is not None: - msg = f"Status set to: {status.value}." - presence["status"] = (status, msg) - return presence + return activity, status - @tasks.loop(minutes=45) + @tasks.loop(minutes=30) async def loop_presence(self): """Set presence to the configured value every 45 minutes.""" - # TODO: Does this even work? - presence = await self.set_presence() - logger.debug("Loop... %s - %s", presence["activity"][1], presence["status"][1]) + logger.debug("Resetting presence.") + await self.set_presence() @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() logger.line() - presence = await self.set_presence() - logger.info(presence["activity"][1]) - logger.info(presence["status"][1]) - await asyncio.sleep(2700) + activity, status = await self.set_presence() + + if activity is not None: + msg = f"Activity set to: {activity.type.name.capitalize()} " + if activity.type == ActivityType.listening: + msg += f"to {activity.name}." + else: + msg += f"{activity.name}." + logger.info(msg) + else: + logger.info("No activity has been set.") + if status is not None: + msg = f"Status set to: {status.value}." + logger.info(msg) + else: + logger.info("No status has been set.") + + await asyncio.sleep(1800) + logger.info("Starting presence loop.") @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) @@ -1789,6 +1759,8 @@ async def oauth_show(self, ctx): async def eval_(self, ctx, *, body: str): """Evaluates Python code.""" + logger.warning("Running eval command:\n%s", body) + env = { "ctx": ctx, "bot": self.bot, diff --git a/core/config.py b/core/config.py index e83751aecc..775781eeb9 100644 --- a/core/config.py +++ b/core/config.py @@ -120,6 +120,8 @@ class ConfigManager: "enable_plugins", } + special_types = {"status", "activity_type"} + defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) @@ -236,6 +238,26 @@ def get(self, key: str, convert=True) -> typing.Any: except ValueError: value = self.remove(key) + elif key in self.special_types: + if value is None: + return + + if key == "status": + try: + # noinspection PyArgumentList + value = discord.Status(value) + except ValueError: + logger.warning("Invalid status %s.", value) + value = self.remove(key) + + elif key == "activity_type": + try: + # noinspection PyArgumentList + value = discord.ActivityType(value) + except ValueError: + logger.warning("Invalid activity %s.", value) + value = self.remove(key) + return value def set(self, key: str, item: typing.Any, convert=True) -> None: @@ -294,6 +316,9 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: except ValueError: raise InvalidConfigError("Must be a yes/no value.") + # elif key in self.special_types: + # if key == "status": + return self.__setitem__(key, item) def remove(self, key: str) -> typing.Any: diff --git a/core/thread.py b/core/thread.py index 207fdae97d..13669739b5 100644 --- a/core/thread.py +++ b/core/thread.py @@ -661,8 +661,6 @@ async def send( url=message.jump_url, ) - delete_message = not bool(message.attachments) - ext = [(a.url, a.filename) for a in message.attachments] images = [] @@ -736,6 +734,12 @@ async def send( embed.set_footer(text=mod_tag) # Normal messages else: embed.set_footer(text=self.bot.config["anon_tag"]) + delete_message = not bool(message.attachments) + if delete_message: + try: + await message.delete() + except Exception as e: + logger.warning('Cannot delete message: %s.', str(e)) elif note: embed.colour = self.bot.main_color else: @@ -760,12 +764,6 @@ async def send( await asyncio.gather(*additional_images) self.ready = True - if delete_message: - try: - if isinstance(message.channel, discord.TextChannel): - await message.delete() - except Exception: - logger.warning('Cannot delete message.', exc_info=True) return msg def get_notifications(self) -> str: From 473cb9e7945579b322c128d9b7a054206f28deeb Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 10 Oct 2019 16:52:24 -0700 Subject: [PATCH 29/48] Catch an error --- bot.py | 2 +- core/thread.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 07a4bc5096..686d7c3667 100644 --- a/bot.py +++ b/bot.py @@ -911,7 +911,7 @@ async def on_raw_reaction_add(self, payload): try: message = await channel.fetch_message(payload.message_id) - except discord.NotFound: + except (discord.NotFound, discord.Forbidden): return reaction = payload.emoji diff --git a/core/thread.py b/core/thread.py index 13669739b5..95991c8c51 100644 --- a/core/thread.py +++ b/core/thread.py @@ -735,7 +735,7 @@ async def send( else: embed.set_footer(text=self.bot.config["anon_tag"]) delete_message = not bool(message.attachments) - if delete_message: + if delete_message and isinstance(message.channel, discord.TextChannel): try: await message.delete() except Exception as e: From c5cca62d82ee571157a692ec733c2b9f4a2d6826 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 10 Oct 2019 17:22:06 -0700 Subject: [PATCH 30/48] Modified logging outputs --- bot.py | 26 ++++++++++++++------------ cogs/plugins.py | 4 ++-- cogs/utility.py | 2 +- core/config.py | 2 +- core/models.py | 17 ++++++++++------- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/bot.py b/bot.py index 686d7c3667..30e631fb65 100644 --- a/bot.py +++ b/bot.py @@ -46,7 +46,8 @@ ch = logging.StreamHandler(stream=sys.stdout) ch.setLevel(logging.INFO) -formatter = logging.Formatter("%(filename)s[%(lineno)d] - %(levelname)s: %(message)s") +formatter = logging.Formatter("%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%b %d %H:%M:%S") ch.setFormatter(formatter) logger.addHandler(ch) @@ -122,16 +123,16 @@ def startup(self): logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") - logger.line() + logger.line('debug') for cog in self.loaded_cogs: - logger.info("Loading %s.", cog) + logger.debug("Loading %s.", cog) try: self.load_extension(cog) - logger.info("Successfully loaded %s.", cog) + logger.debug("Successfully loaded %s.", cog) except Exception: logger.exception("Failed to load %s.", cog) - logger.line() + logger.line('debug') def _configure_logging(self): level_text = self.config["log_level"].upper() @@ -409,14 +410,13 @@ def command_perm(self, command_name: str) -> PermissionLevel: return level async def on_connect(self): - logger.line() try: await self.validate_database_connection() except Exception: logger.debug("Logging out due to failed database connection.") return await self.logout() - logger.info("Connected to gateway.") + logger.debug("Connected to gateway.") await self.config.refresh() await self.setup_indexes() self._connected.set() @@ -457,10 +457,11 @@ async def on_ready(self): return await self.logout() logger.line() - logger.info("Client ready.") - logger.line() + logger.debug("Client ready.") logger.info("Logged in as: %s", self.user) - logger.info("User ID: %s", self.user.id) + logger.info("Bot ID: %s", self.user.id) + owners = ", ".join(getattr(self.get_user(owner_id), "name", str(owner_id)) for owner_id in self.owner_ids) + logger.info("Owners: %s", owners) logger.info("Prefix: %s", self.prefix) logger.info("Guild Name: %s", self.guild.name) logger.info("Guild ID: %s", self.guild.id) @@ -1099,7 +1100,8 @@ async def validate_database_connection(self): ) raise else: - logger.info("Successfully connected to the database.") + logger.debug("Successfully connected to the database.") + logger.line('debug') async def post_metadata(self): owner = (await self.application_info()).owner @@ -1125,7 +1127,7 @@ async def post_metadata(self): async def before_post_metadata(self): await self.wait_for_connected() logger.debug("Starting metadata loop.") - logger.line() + logger.line('debug') if not self.guild: self.metadata_loop.cancel() diff --git a/cogs/plugins.py b/cogs/plugins.py index c52be27192..52fc20bdd6 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -147,7 +147,7 @@ async def initial_load_plugins(self): logger.error("Error when loading plugin %s.", plugin, exc_info=True) continue - logger.info("Finished loading all plugins.") + logger.debug("Finished loading all plugins.") self._ready_event.set() await self.bot.config.update() @@ -226,7 +226,7 @@ async def load_plugin(self, plugin): try: self.bot.load_extension(plugin.ext_string) - logger.info("Loaded plugin: %s", plugin.ext_string) + logger.info("Loaded plugin: %s", plugin.ext_string.split('.')[-1]) self.loaded_plugins.add(plugin) except commands.ExtensionError as exc: diff --git a/cogs/utility.py b/cogs/utility.py index d8dd9f97a5..a709ac469b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -603,7 +603,7 @@ async def loop_presence(self): @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() - logger.line() + logger.line('debug') activity, status = await self.set_presence() if activity is not None: diff --git a/core/config.py b/core/config.py index 775781eeb9..781a5c23df 100644 --- a/core/config.py +++ b/core/config.py @@ -182,7 +182,7 @@ async def refresh(self) -> dict: self._cache[k] = v if not self.ready_event.is_set(): self.ready_event.set() - logger.info("Successfully fetched configurations from database.") + logger.debug("Successfully fetched configurations from database.") return self._cache async def wait_until_ready(self) -> None: diff --git a/core/models.py b/core/models.py index 033457a542..47e6375cc5 100644 --- a/core/models.py +++ b/core/models.py @@ -44,7 +44,7 @@ def _debug_(*msgs): @staticmethod def _info_(*msgs): - return f'{Fore.GREEN}{" ".join(msgs)}{Style.RESET_ALL}' + return f'{Fore.LIGHTMAGENTA_EX}{" ".join(msgs)}{Style.RESET_ALL}' @staticmethod def _error_(*msgs): @@ -70,13 +70,16 @@ def critical(self, msg, *args, **kwargs): if self.isEnabledFor(logging.CRITICAL): self._log(logging.CRITICAL, self._error_(msg), args, **kwargs) - def exception(self, msg, *args, exc_info=True, **kwargs): - self.error(msg, *args, exc_info=exc_info, **kwargs) - - def line(self): - if self.isEnabledFor(logging.INFO): + def line(self, level="info"): + if level == "info": + level = logging.INFO + elif level == "debug": + level = logging.DEBUG + else: + level = logging.INFO + if self.isEnabledFor(level): self._log( - logging.INFO, + level, Fore.BLACK + Style.BRIGHT + "-------------------------" From 3f9b824fc78efe3d0aab79f42204831ff3f5865c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 10 Oct 2019 17:36:58 -0700 Subject: [PATCH 31/48] Updated registry --- bot.py | 10 +++++----- plugins/registry.json | 9 --------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/bot.py b/bot.py index 30e631fb65..d94c239360 100644 --- a/bot.py +++ b/bot.py @@ -73,7 +73,7 @@ def __init__(self): self.metadata_loop = None self.formatter = SafeFormatter() self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] - + self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -144,9 +144,8 @@ def _configure_logging(self): "DEBUG": logging.DEBUG, } - log_file_name = self.token.split(".")[0] ch_debug = logging.FileHandler( - os.path.join(temp_dir, f"{log_file_name}.log"), mode="a+" + self.log_file_name, mode="a+" ) ch_debug.setLevel(logging.DEBUG) @@ -169,6 +168,7 @@ def _configure_logging(self): else: logger.info("Invalid logging level set.") logger.warning("Using default logging level: %s.", level_text) + logger.info("Log file: %s", self.log_file_name) logger.debug("Successfully configured logging.") @property @@ -545,8 +545,8 @@ async def convert_emoji(self, name: str) -> str: if name not in UNICODE_EMOJI: try: name = await converter.convert(ctx, name.strip(":")) - except commands.BadArgument: - logger.warning("%s is not a valid emoji.", name) + except commands.BadArgument as e: + logger.warning("%s is not a valid emoji. %s.", str(e)) raise return name diff --git a/plugins/registry.json b/plugins/registry.json index a7d699fa5a..c03c774b88 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -151,15 +151,6 @@ "title": "Colors!!", "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" - }, - "logger": { - "repository": "Taaku18/modmail-plugins", - "branch": "master", - "description": "Moderation logger, logs message edits, message delete, channel changes, member join, etc.", - "bot_version": "2.20.1", - "title": "Server Activity Logger", - "icon_url": "https://i.imgur.com/dRqPq5M.png", - "thumbnail_url": "https://i.imgur.com/moQn7RC.png" }, "fun": { "repository": "TheKinG2149/modmail-plugins", From b3875bb948fd3c5a8476cef81433a7db3bf7e3dd Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 10 Oct 2019 17:52:01 -0700 Subject: [PATCH 32/48] Whoops --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index d94c239360..a5287d69cd 100644 --- a/bot.py +++ b/bot.py @@ -73,7 +73,6 @@ def __init__(self): self.metadata_loop = None self.formatter = SafeFormatter() self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] - self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -82,6 +81,7 @@ def __init__(self): self.threads = ThreadManager(self) + self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") self._configure_logging() mongo_uri = self.config["mongo_uri"] From 937d6fdf26928d937c0e2b78ec13ed02179fe580 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 14 Oct 2019 21:31:21 -0700 Subject: [PATCH 33/48] Fix block --- CHANGELOG.md | 6 ++++ bot.py | 16 ++++++++-- cogs/modmail.py | 77 ++++++++++++++++++------------------------------- 3 files changed, 47 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index add6bb5d05..78e4b9625e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ however, insignificant breaking changes does not guarantee a major version bump, # v3.3.0-dev2 + +### Important + +- Recommend all users to unblock and re-block all blocked users upon updating to this release. + ### Added - Three new config vars: @@ -39,6 +44,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. - Eval commands are logged in debug logs. - Presence updates 30 minutes instead of 45 now. +- Fixed an assortment of problems to do with block. ### Internal diff --git a/bot.py b/bot.py index a5287d69cd..f01be93590 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev2" +__version__ = "3.3.0-dev3" import asyncio import logging @@ -676,9 +676,16 @@ async def _process_blocked(self, message: discord.Message) -> bool: self.blocked_users.pop(str(message.author.id)) else: reaction = blocked_emoji - end_time = re.search(r"%(.+?)%", reason) + # etc "blah blah blah... until 2019-10-14T21:12:45.559948." + end_time = re.search(r"until ([^`]+?)\.$", reason) + if end_time is None: + # backwards compat + end_time = re.search(r"%([^%]+?)%", reason) + if end_time is not None: + logger.warning(r"Deprecated time message for user %s, block and unblock again to update.", + message.author) + if end_time is not None: - logger.debug("No longer blocked, user %s.", message.author.name) after = ( datetime.fromisoformat(end_time.group(1)) - now ).total_seconds() @@ -686,6 +693,9 @@ async def _process_blocked(self, message: discord.Message) -> bool: # No longer blocked reaction = sent_emoji self.blocked_users.pop(str(message.author.id)) + logger.debug("No longer blocked, user %s.", message.author.name) + else: + logger.debug("User blocked, user %s.", message.author.name) else: logger.debug("User blocked, user %s.", message.author.name) else: diff --git a/cogs/modmail.py b/cogs/modmail.py index 67e840bded..d6911092af 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -953,15 +953,15 @@ async def blocked(self, ctx): else: try: user = await self.bot.fetch_user(id_) - users.append((str(user), reason)) + users.append((user.mention, reason)) except discord.NotFound: - pass + users.append((id_, reason)) if users: embed = embeds[0] for mention, reason in users: - line = mention + f" - `{reason or 'No reason provided'}`\n" + line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( title="Blocked Users (Continued)", @@ -1008,9 +1008,7 @@ async def blocked_whitelist(self, ctx, *, user: User = None): self.bot.blocked_whitelisted_users.append(str(user.id)) if str(user.id) in self.bot.blocked_users: - msg = self.bot.blocked_users.get(str(user.id)) - if msg is None: - msg = "" + msg = self.bot.blocked_users.get(str(user.id)) or "" self.bot.blocked_users.pop(str(user.id)) await self.bot.config.update() @@ -1018,10 +1016,10 @@ async def blocked_whitelist(self, ctx, *, user: User = None): if msg.startswith("System Message: "): # If the user is blocked internally (for example: below minimum account age) # Show an extended message stating the original internal message - reason = msg[16:].strip().rstrip(".") or "no reason" + reason = msg[16:].strip().rstrip(".") embed = discord.Embed( title="Success", - description=f"{mention} was previously blocked internally due to " + description=f"{mention} was previously blocked internally for " f'"{reason}". {mention} is now whitelisted.', color=self.bot.main_color, ) @@ -1051,8 +1049,6 @@ async def block( `duration` may be a simple "human-readable" time text. See `{prefix}help close` for examples. """ - reason = "" - if user is None: thread = ctx.thread if thread: @@ -1064,8 +1060,6 @@ async def block( mention = getattr(user, "mention", f"`{user.id}`") - moderator = ctx.author.name - if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( title="Error", @@ -1074,53 +1068,38 @@ async def block( ) return await ctx.send(embed=embed) + reason = f"by {escape_markdown(ctx.author.name)}#{ctx.author.discriminator}" + if after is not None: - reason = f"{after.arg} by {moderator}" - if reason.startswith("System Message: "): - raise commands.BadArgument( - "The reason cannot start with `System Message:`." - ) if "%" in reason: raise commands.BadArgument('The reason contains illegal character "%".') + if after.arg: + reason += f" for `{after.arg}`" if after.dt > after.now: - reason = f"{reason} %{after.dt.isoformat()}% by {moderator}" + reason += f" until {after.dt.isoformat()}" - if not reason: - reason = f"Blocked by {moderator}" + reason += "." - extend = f" for `{reason}`" if reason is not None else "" msg = self.bot.blocked_users.get(str(user.id)) if msg is None: msg = "" - if ( - str(user.id) not in self.bot.blocked_users - or reason is not None - or msg.startswith("System Message: ") - ): - if str(user.id) in self.bot.blocked_users: - - old_reason = msg.strip().rstrip(".") or f"Blocked by {moderator}" - embed = discord.Embed( - title="Success", - description=f"{mention} was previously blocked for " - f'"{old_reason}". {mention} is now blocked{extend}.', - color=self.bot.main_color, - ) - else: - embed = discord.Embed( - title="Success", - color=self.bot.main_color, - description=f"{mention} is now blocked{extend}.", - ) - self.bot.blocked_users[str(user.id)] = reason - await self.bot.config.update() + if str(user.id) in self.bot.blocked_users and msg: + old_reason = msg.strip().rstrip(".") + embed = discord.Embed( + title="Success", + description=f"{mention} was previously blocked " + f'{old_reason}.\n{mention} is now blocked {reason}', + color=self.bot.main_color, + ) else: embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"{mention} is already blocked.", + title="Success", + color=self.bot.main_color, + description=f"{mention} is now blocked {reason}", ) + self.bot.blocked_users[str(user.id)] = reason + await self.bot.config.update() return await ctx.send(embed=embed) @@ -1156,12 +1135,12 @@ async def unblock(self, ctx, *, user: User = None): reason = msg[16:].strip().rstrip(".") or "no reason" embed = discord.Embed( title="Success", - description=f"{mention} was previously blocked internally due to " - f'"{reason}". {mention} is no longer blocked.', + description=f"{mention} was previously blocked internally " + f'{reason}.\n{mention} is no longer blocked.', color=self.bot.main_color, ) embed.set_footer( - text="However, if the original system block reason still apply, " + text="However, if the original system block reason still applies, " f"{name} will be automatically blocked again. Use " f'"{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' ) From 58e25b0ebfc4e11ad75bd1432cfb08c1b784f391 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 22 Oct 2019 23:08:08 -0700 Subject: [PATCH 34/48] Fix travis ci --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 65654b83fc..21035e6f3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,13 +8,13 @@ matrix: name: "Python 3.6.7 on Xenial Linux" - name: "Python 3.7.4 on macOS" os: osx - osx_image: xcode11 + osx_image: xcode11.2 language: shell - - name: "Python 3.7.4 on Windows" + - name: "Python 3.7.5 on Windows" os: windows language: shell before_install: - - choco install python + - choco install python --version=3.7.5 - python -m pip install --upgrade pip env: PATH=/c/Python37:/c/Python37/Scripts:$PATH From af87c8b3cdc98ffa5cf3212dbdccd264a388e045 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 22 Oct 2019 23:24:36 -0700 Subject: [PATCH 35/48] Updated changelog version regex for prerelease tag --- core/changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/changelog.py b/core/changelog.py index 3e48a41f97..1e9254286f 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -130,7 +130,7 @@ class Changelog: """ VERSION_REGEX = re.compile( - r"#\s*([vV]\d+\.\d+(?:\.\d+)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)?|$)", + r"#\s*([vV]\d+\.\d+(?:\.\d+)?(?:-\w+?)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)(?:-\w+?)?|$)", flags=re.DOTALL, ) From 36407ffecbda034542521703791a660dace05caf Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 22 Oct 2019 23:35:59 -0700 Subject: [PATCH 36/48] Add prerelease notice in about footer --- cogs/utility.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index a709ac469b..57c536f4ea 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -304,7 +304,10 @@ async def about(self, ctx): changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version - if self.bot.version < parse_version(latest.version): + if self.bot.version.is_prerelease: + stable = next(filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions)) + footer = f"You are on the prerelease version • the latest version is v{stable.version}." + elif self.bot.version < parse_version(latest.version): footer = f"A newer version is available v{latest.version}." else: footer = "You are up to date with the latest version." From 579f576b01b68d09eec5cb0cf3fa0cd01bc7cdd5 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 24 Oct 2019 20:41:19 -0700 Subject: [PATCH 37/48] Bump discord.py version to 1.2.4 --- Pipfile | 2 +- Pipfile.lock | 52 +++++++++++++++++++++++++++++----------------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Pipfile b/Pipfile index 7b54ff37b2..b794958a5c 100644 --- a/Pipfile +++ b/Pipfile @@ -22,7 +22,7 @@ parsedatetime = "==2.4" aiohttp = "<3.6.0,>=3.3.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = "==1.2.3" +"discord.py" = "==1.2.4" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 431469cdcf..5b21468ed8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f5ceea517895626871c9bb2c559e39f6b81859f55669897aa9533154a4df1431" + "sha256": "e218eea19b5d78a490cf012fb818e002a128bbed21b08f2327963e66c2d47c71" }, "pipfile-spec": 6, "requires": { @@ -53,10 +53,10 @@ }, "attrs": { "hashes": [ - "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", - "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.2.0" + "version": "==19.3.0" }, "certifi": { "hashes": [ @@ -82,10 +82,11 @@ }, "discord.py": { "hashes": [ - "sha256:4684733fa137cc7def18087ae935af615212e423e3dbbe3e84ef01d7ae8ed17d" + "sha256:2d5fec14b1c047b561336e969939e7f34564489851ec1a7edb8d303087127256", + "sha256:3e044d84f0bb275d173e2d958cb4a579e525707f90e3e8a15c59901f79e80663" ], "index": "pypi", - "version": "==1.2.3" + "version": "==1.2.4" }, "dnspython": { "hashes": [ @@ -104,9 +105,9 @@ }, "future": { "hashes": [ - "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" + "sha256:858e38522e8fd0d3ce8f0c1feaf0603358e366d5403209674c7b617fa0c24093" ], - "version": "==0.17.1" + "version": "==0.18.1" }, "idna": { "hashes": [ @@ -261,10 +262,10 @@ }, "virtualenv": { "hashes": [ - "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", - "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" + "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", + "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" ], - "version": "==16.7.5" + "version": "==16.7.7" }, "virtualenv-clone": { "hashes": [ @@ -326,17 +327,17 @@ }, "astroid": { "hashes": [ - "sha256:98c665ad84d10b18318c5ab7c3d203fe11714cbad2a4aef4f44651f415392754", - "sha256:b7546ffdedbf7abcfbff93cd1de9e9980b1ef744852689decc5aeada324238c6" + "sha256:09a3fba616519311f1af8a461f804b68f0370e100c9264a035aa7846d7852e33", + "sha256:5a79c9b4bd6c4be777424593f957c996e20beb5f74e0bc332f47713c6f675efe" ], - "version": "==2.3.1" + "version": "==2.3.2" }, "attrs": { "hashes": [ - "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", - "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.2.0" + "version": "==19.3.0" }, "bandit": { "hashes": [ @@ -385,10 +386,10 @@ }, "gitpython": { "hashes": [ - "sha256:631263cc670aa56ce3d3c414cf0fe2e840f2e913514b138ea28d88a477bbcd21", - "sha256:6e97b9f0954807f30c2dd8e3165731ed6c477a1b365f194b69d81d7940a08332" + "sha256:3237caca1139d0a7aa072f6735f5fd2520de52195e0fa1d8b83a9b212a2498b2", + "sha256:a7d6bef0775f66ba47f25911d285bcd692ce9053837ff48a120c2b8cf3a71389" ], - "version": "==3.0.3" + "version": "==3.0.4" }, "isort": { "hashes": [ @@ -450,11 +451,11 @@ }, "pylint": { "hashes": [ - "sha256:7edbae11476c2182708063ac387a8f97c760d9cfe36a5ede0ca996f90cf346c8", - "sha256:844ce067788028c1a35086a5c66bc5e599ddd851841c41d6ee1623b36774d9f2" + "sha256:7b76045426c650d2b0f02fc47c14d7934d17898779da95288a74c2a7ec440702", + "sha256:856476331f3e26598017290fd65bebe81c960e806776f324093a46b76fb2d1c0" ], "index": "pypi", - "version": "==2.4.2" + "version": "==2.4.3" }, "pyyaml": { "hashes": [ @@ -504,20 +505,25 @@ }, "typed-ast": { "hashes": [ + "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", + "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", + "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", + "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", + "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" ], "markers": "implementation_name == 'cpython' and python_version < '3.8'", From 53d9fc60b17aaffe678463ab7178699da572a8a6 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 24 Oct 2019 20:54:54 -0700 Subject: [PATCH 38/48] Updated requirements.min.txt and runtime.txt --- README.md | 2 +- requirements.min.txt | 16 +++++++++++++--- runtime.txt | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2fa070457b..763f814c1b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
diff --git a/requirements.min.txt b/requirements.min.txt index 2e9086e887..c8c5c2ab07 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -1,14 +1,24 @@ -# Generated as of July 11, 2019 +# Generated as of October, 2019 # This is the bare minimum requirements.txt for running Modmail. # To install requirements.txt run: pip install -r requirements.min.txt aiohttp==3.5.4 -discord.py==1.2.3 +async-timeout==3.0.1 +attrs==19.3.0 +chardet==3.0.4 +discord.py==1.2.4 dnspython==1.16.0 -emoji==0.5.2 +emoji==0.5.4 +future==0.18.1 +idna==2.8 isodate==0.6.0 motor==2.0.0 +multidict==4.5.2 natural==0.2.0 parsedatetime==2.4 +pymongo==3.9.0 python-dateutil==2.8.0 python-dotenv==0.10.3 +six==1.12.0 +websockets==6.0 +yarl==1.3.0 diff --git a/runtime.txt b/runtime.txt index 42731f2fbe..aefcfbece7 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.4 +python-3.7.5 From 07d1bfd453df871cde303832326d4ac7c9b25ea0 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 25 Oct 2019 22:00:58 -0700 Subject: [PATCH 39/48] Fixed a delete message oversight --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 95991c8c51..fd4312fb80 100644 --- a/core/thread.py +++ b/core/thread.py @@ -735,7 +735,7 @@ async def send( else: embed.set_footer(text=self.bot.config["anon_tag"]) delete_message = not bool(message.attachments) - if delete_message and isinstance(message.channel, discord.TextChannel): + if delete_message and isinstance(destination, discord.TextChannel): try: await message.delete() except Exception as e: From 7c823160f2d9c146cc0c3f4e51646b6e3bfa15d2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 30 Oct 2019 00:53:36 -0700 Subject: [PATCH 40/48] Fixed a typo --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index d6911092af..699999c205 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -309,7 +309,7 @@ async def move( await thread.channel.edit(category=category, sync_permissions=True) - if self.bot.config("thread_move_notify") and not silent: + if self.bot.config["thread_move_notify"] and not silent: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], From 45871710049d79b5d9dc97641224e40dd50dda1d Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 30 Oct 2019 00:57:10 -0700 Subject: [PATCH 41/48] Use channel check instead for delete --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index fd4312fb80..ed92bc5dfe 100644 --- a/core/thread.py +++ b/core/thread.py @@ -735,7 +735,7 @@ async def send( else: embed.set_footer(text=self.bot.config["anon_tag"]) delete_message = not bool(message.attachments) - if delete_message and isinstance(destination, discord.TextChannel): + if delete_message and destination == self.channel: try: await message.delete() except Exception as e: From 5f389596e61121b9e2e2fbccf651efa579c5488c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 31 Oct 2019 00:09:51 -0700 Subject: [PATCH 42/48] Delete note command --- CHANGELOG.md | 3 ++- core/thread.py | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e4b9625e..59b38ee9b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev2 +# v3.3.0-dev3 ### Important @@ -31,6 +31,7 @@ however, insignificant breaking changes does not guarantee a major version bump, ### Changed - `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) +- Automatically delete notes command `?note` when there's no attachments attached. - Plugins update (mostly internal). - `git` is no longer used to install plugins, it now downloads through zip files. - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. diff --git a/core/thread.py b/core/thread.py index ed92bc5dfe..41f77a82c4 100644 --- a/core/thread.py +++ b/core/thread.py @@ -734,17 +734,19 @@ async def send( embed.set_footer(text=mod_tag) # Normal messages else: embed.set_footer(text=self.bot.config["anon_tag"]) + elif note: + embed.colour = self.bot.main_color + else: + embed.set_footer(text=f"Message ID: {message.id}") + embed.colour = self.bot.recipient_color + + if from_mod or note: delete_message = not bool(message.attachments) if delete_message and destination == self.channel: try: await message.delete() except Exception as e: logger.warning('Cannot delete message: %s.', str(e)) - elif note: - embed.colour = self.bot.main_color - else: - embed.set_footer(text=f"Message ID: {message.id}") - embed.colour = self.bot.recipient_color try: await destination.trigger_typing() From 2e98510ea668eec4a0547a520038ec6a52e0078b Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 31 Oct 2019 00:48:53 -0700 Subject: [PATCH 43/48] Fixed author links --- CHANGELOG.md | 6 +++++- bot.py | 2 +- core/thread.py | 7 ++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b38ee9b5..540b98e3a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev3 +# v3.3.0-dev4 ### Important @@ -32,6 +32,10 @@ however, insignificant breaking changes does not guarantee a major version bump, - `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) - Automatically delete notes command `?note` when there's no attachments attached. +- Embed author links used to be inaccessible in many cases, now: + - `?anonreply`, `?reply`, and `?note` in thread channel will link to the sender's profile. + - `?reply` and recipient's DM will also link the sender's profile. + - `?anonreply` in DM channel will link to the first channel of the main guild. - Plugins update (mostly internal). - `git` is no longer used to install plugins, it now downloads through zip files. - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. diff --git a/bot.py b/bot.py index acb4f89550..451dbc93ca 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev3" +__version__ = "3.3.0-dev4" import asyncio import logging diff --git a/core/thread.py b/core/thread.py index 41f77a82c4..056f3c3073 100644 --- a/core/thread.py +++ b/core/thread.py @@ -647,18 +647,19 @@ async def send( avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.guild.icon_url + embed.set_author(name=name, icon_url=avatar_url, + url=f"https://discordapp.com/channels/{self.bot.guild.id}") else: # Normal message name = str(author) avatar_url = author.avatar_url - - embed.set_author(name=name, icon_url=avatar_url, url=message.jump_url) + embed.set_author(name=name, icon_url=avatar_url, url=f"https://discordapp.com/users/{author.id}") else: # Special note messages embed.set_author( name=f"Note ({author.name})", icon_url=system_avatar_url, - url=message.jump_url, + url=f"https://discordapp.com/users/{author.id}" ) ext = [(a.url, a.filename) for a in message.attachments] From 351ee43c7d0267806886d5b5a40342782b3b3717 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 1 Nov 2019 01:48:51 -0700 Subject: [PATCH 44/48] Add ability to disable DM --- CHANGELOG.md | 12 ++++ bot.py | 127 +++++++++++++++++++++++++++++------------- cogs/modmail.py | 99 ++++++++++++++++++++++++++++++-- core/config.py | 9 +++ core/config_help.json | 66 ++++++++++++++++++++++ core/thread.py | 28 ++++++++-- 6 files changed, 293 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 540b98e3a1..438227c419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,18 @@ however, insignificant breaking changes does not guarantee a major version bump, - `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) - `user` when not provided, defaults to the user who ran the command. - Open threads in limbo now auto closes if the channel cannot be found. This check is done every time the bot restarts. +- Ability to disable new threads from getting created. + - `?disable` +- Ability to fully disable Modmail DM. + - `?disable all` +- To re-enable DM: `?enable`, and to see the current status: `?isenable`. +- This disabled Modmail interface is customizable with the following config vars: + - `disabled_new_thread_title` + - `disabled_new_thread_response` + - `disabled_new_thread_footer` + - `disabled_current_thread_title` + - `disabled_current_thread_response` + - `disabled_current_thread_footer` ### Changed diff --git a/bot.py b/bot.py index 451dbc93ca..34df9f1bb5 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev4" +__version__ = "3.3.0-dev5" import asyncio import logging @@ -46,8 +46,10 @@ ch = logging.StreamHandler(stream=sys.stdout) ch.setLevel(logging.INFO) -formatter = logging.Formatter("%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", - datefmt="%b %d %H:%M:%S") +formatter = logging.Formatter( + "%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%b %d %H:%M:%S", +) ch.setFormatter(formatter) logger.addHandler(ch) @@ -123,7 +125,7 @@ def startup(self): logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") - logger.line('debug') + logger.line("debug") for cog in self.loaded_cogs: logger.debug("Loading %s.", cog) @@ -132,7 +134,7 @@ def startup(self): logger.debug("Successfully loaded %s.", cog) except Exception: logger.exception("Failed to load %s.", cog) - logger.line('debug') + logger.line("debug") def _configure_logging(self): level_text = self.config["log_level"].upper() @@ -144,9 +146,7 @@ def _configure_logging(self): "DEBUG": logging.DEBUG, } - ch_debug = logging.FileHandler( - self.log_file_name, mode="a+" - ) + ch_debug = logging.FileHandler(self.log_file_name, mode="a+") ch_debug.setLevel(logging.DEBUG) formatter_debug = FileFormatter( @@ -460,7 +460,10 @@ async def on_ready(self): logger.debug("Client ready.") logger.info("Logged in as: %s", self.user) logger.info("Bot ID: %s", self.user.id) - owners = ", ".join(getattr(self.get_user(owner_id), "name", str(owner_id)) for owner_id in self.owner_ids) + owners = ", ".join( + getattr(self.get_user(owner_id), "name", str(owner_id)) + for owner_id in self.owner_ids + ) logger.info("Owners: %s", owners) logger.info("Prefix: %s", self.prefix) logger.info("Guild Name: %s", self.guild.name) @@ -504,10 +507,12 @@ async def on_ready(self): ) for log in await self.api.get_open_logs(): - if self.get_channel(int(log['channel_id'])) is None: - logger.debug("Unable to resolve thread with channel %s.", log['channel_id']) + if self.get_channel(int(log["channel_id"])) is None: + logger.debug( + "Unable to resolve thread with channel %s.", log["channel_id"] + ) log_data = await self.api.post_log( - log['channel_id'], + log["channel_id"], { "open": False, "closed_at": str(datetime.utcnow()), @@ -522,9 +527,14 @@ async def on_ready(self): }, ) if log_data: - logger.debug("Successfully closed thread with channel %s.", log['channel_id']) + logger.debug( + "Successfully closed thread with channel %s.", log["channel_id"] + ) else: - logger.debug("Failed to close thread with channel %s, skipping.", log['channel_id']) + logger.debug( + "Failed to close thread with channel %s, skipping.", + log["channel_id"], + ) self.metadata_loop = tasks.Loop( self.post_metadata, @@ -573,7 +583,9 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: return sent_emoji, blocked_emoji - async def _process_blocked(self, message: discord.Message) -> bool: + async def _process_blocked( + self, message: discord.Message + ) -> typing.Tuple[bool, str]: sent_emoji, blocked_emoji = await self.retrieve_emoji() if str(message.author.id) in self.blocked_whitelisted_users: @@ -581,13 +593,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: self.blocked_users.pop(str(message.author.id)) await self.config.update() - if sent_emoji != "disable": - try: - await message.add_reaction(sent_emoji) - except (discord.HTTPException, discord.InvalidArgument): - logger.warning("Failed to add sent_emoji.", exc_info=True) - - return False + return False, sent_emoji now = datetime.utcnow() @@ -682,8 +688,10 @@ async def _process_blocked(self, message: discord.Message) -> bool: # backwards compat end_time = re.search(r"%([^%]+?)%", reason) if end_time is not None: - logger.warning(r"Deprecated time message for user %s, block and unblock again to update.", - message.author) + logger.warning( + r"Deprecated time message for user %s, block and unblock again to update.", + message.author, + ) if end_time is not None: after = ( @@ -702,19 +710,62 @@ async def _process_blocked(self, message: discord.Message) -> bool: reaction = sent_emoji await self.config.update() + return str(message.author.id) in self.blocked_users, reaction + + @staticmethod + async def add_reaction(msg, reaction): if reaction != "disable": try: - await message.add_reaction(reaction) + await msg.add_reaction(reaction) except (discord.HTTPException, discord.InvalidArgument): logger.warning("Failed to add reaction %s.", reaction, exc_info=True) - return str(message.author.id) in self.blocked_users async def process_dm_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" - blocked = await self._process_blocked(message) - if not blocked: - thread = await self.threads.find_or_create(message.author) - await thread.send(message) + blocked, reaction = await self._process_blocked(message) + if blocked: + return await self.add_reaction(message, reaction) + thread = await self.threads.find(recipient=message.author) + if thread is None: + if self.config["dm_disabled"] >= 1: + embed = discord.Embed( + title=self.config["disabled_new_thread_title"], + color=self.error_color, + description=self.config["disabled_new_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], + icon_url=self.guild.icon_url, + ) + logger.info( + "A new thread was blocked from %s due to disabled Modmail.", + message.author, + ) + _, blocked_emoji = await self.retrieve_emoji() + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + thread = self.threads.create(message.author) + else: + if self.config["dm_disabled"] == 2: + embed = discord.Embed( + title=self.config["disabled_current_thread_title"], + color=self.error_color, + description=self.config["disabled_current_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_current_thread_footer"], + icon_url=self.guild.icon_url, + ) + logger.info( + "A message was blocked from %s due to disabled Modmail.", + message.author, + ) + _, blocked_emoji = await self.retrieve_emoji() + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + + await self.add_reaction(message, reaction) + await thread.send(message) async def get_contexts(self, message, *, cls=commands.Context): """ @@ -898,13 +949,13 @@ async def _void(*_args, **_kwargs): thread = await self.threads.find(channel=channel) if thread is not None and thread.recipient: - if await self._process_blocked( - SimpleNamespace( - author=thread.recipient, - channel=SimpleNamespace(send=_void), - add_reaction=_void, + if ( + await self._process_blocked( + SimpleNamespace( + author=thread.recipient, channel=SimpleNamespace(send=_void) + ) ) - ): + )[0]: return await thread.recipient.trigger_typing() @@ -1111,7 +1162,7 @@ async def validate_database_connection(self): raise else: logger.debug("Successfully connected to the database.") - logger.line('debug') + logger.line("debug") async def post_metadata(self): owner = (await self.application_info()).owner @@ -1137,7 +1188,7 @@ async def post_metadata(self): async def before_post_metadata(self): await self.wait_for_connected() logger.debug("Starting metadata loop.") - logger.line('debug') + logger.line("debug") if not self.guild: self.metadata_loop.cancel() diff --git a/cogs/modmail.py b/cogs/modmail.py index 699999c205..954e617b5c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -626,9 +626,9 @@ def format_log_embeds(self, logs, avatar_url): embed.add_field( name="Created", value=duration(created_at, now=datetime.utcnow()) ) - closer = entry.get('closer') + closer = entry.get("closer") if closer is None: - closer_msg = 'Unknown' + closer_msg = "Unknown" else: closer_msg = f"<@{closer['id']}>" embed.add_field(name="Closed By", value=closer_msg) @@ -916,6 +916,9 @@ async def contact( thread = self.bot.threads.create( user, creator=ctx.author, category=category ) + if self.bot.config["dm_disabled"] >= 1: + logger.info("Contacting user %s when Modmail DM is disabled.", user) + embed = discord.Embed( title="Created Thread", description=f"Thread started by {ctx.author.mention} " @@ -1089,7 +1092,7 @@ async def block( embed = discord.Embed( title="Success", description=f"{mention} was previously blocked " - f'{old_reason}.\n{mention} is now blocked {reason}', + f"{old_reason}.\n{mention} is now blocked {reason}", color=self.bot.main_color, ) else: @@ -1136,7 +1139,7 @@ async def unblock(self, ctx, *, user: User = None): embed = discord.Embed( title="Success", description=f"{mention} was previously blocked internally " - f'{reason}.\n{mention} is no longer blocked.', + f"{reason}.\n{mention} is no longer blocked.", color=self.bot.main_color, ) embed.set_footer( @@ -1197,6 +1200,94 @@ async def delete(self, ctx, message_id: Optional[int] = None): except (discord.HTTPException, discord.InvalidArgument): pass + @commands.command() + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + async def enable(self, ctx): + """ + Re-enables DM functionalities of Modmail. + + Undo's the `{prefix}disable` command, all DM will be relayed after running this command. + """ + embed = discord.Embed( + title="Success", + description=f"Modmail will now accept all DM messages.", + color=self.bot.main_color, + ) + + if self.bot.config["dm_disabled"] != 0: + self.bot.config["dm_disabled"] = 0 + await self.bot.config.update() + + return await ctx.send(embed=embed) + + @commands.group(invoke_without_command=True) + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + async def disable(self, ctx): + """ + Stop accepting new Modmail threads. + + No new threads can be created through DM. + To stop all existing threads from DMing Modmail, do `{prefix}disable all`. + """ + embed = discord.Embed( + title="Success", + description=f"Modmail will not create any new threads.", + color=self.bot.main_color, + ) + if self.bot.config["dm_disabled"] < 1: + self.bot.config["dm_disabled"] = 1 + await self.bot.config.update() + + return await ctx.send(embed=embed) + + @disable.command(name="all") + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + async def disable_all(self, ctx): + """ + Disables all DM functionalities of Modmail. + + No new threads can be created through DM nor no further DM messages will be relayed. + """ + embed = discord.Embed( + title="Success", + description=f"Modmail will not accept any DM messages.", + color=self.bot.main_color, + ) + + if self.bot.config["dm_disabled"] != 2: + self.bot.config["dm_disabled"] = 2 + await self.bot.config.update() + + return await ctx.send(embed=embed) + + @commands.command() + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + async def isenable(self, ctx): + """ + Check if the DM functionalities of Modmail is enabled. + """ + + if self.bot.config["dm_disabled"] == 1: + embed = discord.Embed( + title="New Threads Disabled", + description=f"Modmail will not create any new threads.", + color=self.bot.main_color, + ) + elif self.bot.config["dm_disabled"] == 2: + embed = discord.Embed( + title="All DM Disabled", + description=f"Modmail will accept any DM messages for new and existing threads.", + color=self.bot.main_color, + ) + else: + embed = discord.Embed( + title="Enabled", + description=f"Modmail is receiving all DM messages.", + color=self.bot.main_color, + ) + + return await ctx.send(embed=embed) + def setup(bot): bot.add_cog(Modmail(bot)) diff --git a/core/config.py b/core/config.py index 781a5c23df..428e6005d3 100644 --- a/core/config.py +++ b/core/config.py @@ -58,6 +58,12 @@ class ConfigManager: "thread_self_close_response": "You have closed this Modmail thread.", "thread_move_notify": False, "thread_move_response": "This thread has been moved.", + "disabled_new_thread_title": "Not Delivered", + "disabled_new_thread_response": "We are not accepting new threads.", + "disabled_new_thread_footer": "Please try again later...", + "disabled_current_thread_title": "Not Delivered", + "disabled_current_thread_response": "We are not accepting any messages.", + "disabled_current_thread_footer": "Please try again later...", # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -73,6 +79,9 @@ class ConfigManager: "activity_message": "", "activity_type": None, "status": None, + # dm_disabled 0 = none, 1 = new threads, 2 = all threads + # TODO: use emum + "dm_disabled": 0, "oauth_whitelist": [], # moderation "blocked": {}, diff --git a/core/config_help.json b/core/config_help.json index cd0167df6f..0a6cee5076 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -348,6 +348,72 @@ "See also: `thread_move_notify`." ] }, + "disabled_new_thread_title": { + "default": "Not Delivered.", + "description": "The title of the message embed when Modmail new thread creation is disabled and user tries to create a new thread.", + "examples": [ + "`{prefix}config set disabled_new_thread_title Closed`" + ], + "notes": [ + "Only has an effect when `{prefix}disable` or `{prefix}disable all` is set.", + "See also: `disabled_new_thread_response`, `disabled_new_thread_footer`, `disabled_current_thread_title`." + ] + }, + "disabled_new_thread_response": { + "default": "We are not accepting new threads.", + "description": "The body of the message embed when Modmail new thread creation is disabled and user tries to create a new thread.", + "examples": [ + "`{prefix}config set disabled_new_thread_response Our working hours is between 8am - 6pm EST.`" + ], + "notes": [ + "Only has an effect when `{prefix}disable` or `{prefix}disable all` is set.", + "See also: `disabled_new_thread_title`, `disabled_new_thread_footer`, `disabled_current_thread_response`." + ] + }, + "disabled_new_thread_footer": { + "default": "Please try again later...", + "description": "The footer of the message embed when Modmail new thread creation is disabled and user tries to create a new thread.", + "examples": [ + "`{prefix}config set disabled_new_thread_footer Contact us later`" + ], + "notes": [ + "Only has an effect when `{prefix}disable` or `{prefix}disable all` is set.", + "See also: `disabled_new_thread_title`, `disabled_new_thread_response`, `disabled_current_thread_footer`." + ] + }, + "disabled_current_thread_title": { + "default": "Not Delivered.", + "description": "The title of the message embed when Modmail DM is disabled and user DMs Modmail from existing thread.", + "examples": [ + "`{prefix}config set disabled_current_thread_title Unavailable`" + ], + "notes": [ + "Only has an effect when `{prefix}disable all` is set.", + "See also: `disabled_current_thread_response`, `disabled_current_thread_footer`, `disabled_new_thread_title`." + ] + }, + "disabled_current_thread_response": { + "default": "We are not accepting any messages.", + "description": "The body of the message embed when Modmail DM is disabled and user DMs Modmail from existing thread.", + "examples": [ + "`{prefix}config set disabled_current_thread_response On break right now.`" + ], + "notes": [ + "Only has an effect when `{prefix}disable all` is set.", + "See also: `disabled_current_thread_title`, `disabled_current_thread_footer`, `disabled_new_thread_response`." + ] + }, + "disabled_current_thread_footer": { + "default": "Please try again later...", + "description": "The footer of the message embed when Modmail DM is disabled and user DMs Modmail from existing thread.", + "examples": [ + "`{prefix}config set disabled_current_thread_footer Message back!`" + ], + "notes": [ + "Only has an effect when `{prefix}disable all` is set.", + "See also: `disabled_current_thread_title`, `disabled_current_thread_response`, `disabled_new_thread_footer`." + ] + }, "recipient_color": { "default": "Discord Gold [#F1C40F](https://placehold.it/100/f1c40f?text=+)", "description": "This is the color of the messages sent by the recipient, this applies to messages received in the thread channel.", diff --git a/core/thread.py b/core/thread.py index 056f3c3073..01523e7d13 100644 --- a/core/thread.py +++ b/core/thread.py @@ -647,19 +647,26 @@ async def send( avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.guild.icon_url - embed.set_author(name=name, icon_url=avatar_url, - url=f"https://discordapp.com/channels/{self.bot.guild.id}") + embed.set_author( + name=name, + icon_url=avatar_url, + url=f"https://discordapp.com/channels/{self.bot.guild.id}", + ) else: # Normal message name = str(author) avatar_url = author.avatar_url - embed.set_author(name=name, icon_url=avatar_url, url=f"https://discordapp.com/users/{author.id}") + embed.set_author( + name=name, + icon_url=avatar_url, + url=f"https://discordapp.com/users/{author.id}", + ) else: # Special note messages embed.set_author( name=f"Note ({author.name})", icon_url=system_avatar_url, - url=f"https://discordapp.com/users/{author.id}" + url=f"https://discordapp.com/users/{author.id}", ) ext = [(a.url, a.filename) for a in message.attachments] @@ -747,7 +754,16 @@ async def send( try: await message.delete() except Exception as e: - logger.warning('Cannot delete message: %s.', str(e)) + logger.warning("Cannot delete message: %s.", str(e)) + + if ( + from_mod + and self.bot.config["dm_disabled"] == 2 + and destination != self.channel + ): + logger.info( + "Sending a message to %s when DM disabled is set.", self.recipient + ) try: await destination.trigger_typing() @@ -814,7 +830,7 @@ async def find( recipient: typing.Union[discord.Member, discord.User] = None, channel: discord.TextChannel = None, recipient_id: int = None, - ) -> Thread: + ) -> typing.Optional[Thread]: """Finds a thread from cache or from discord channel topics.""" if recipient is None and channel is not None: thread = self._find_from_channel(channel) From 320fa67da06a521a28342c3d6626fa9af2918fc2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 1 Nov 2019 07:20:21 -0700 Subject: [PATCH 45/48] Typo --- CHANGELOG.md | 2 +- cogs/modmail.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 438227c419..f2e8313c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev4 +# v3.3.0-dev5 ### Important diff --git a/cogs/modmail.py b/cogs/modmail.py index 954e617b5c..62f36ac6dd 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1270,19 +1270,19 @@ async def isenable(self, ctx): if self.bot.config["dm_disabled"] == 1: embed = discord.Embed( title="New Threads Disabled", - description=f"Modmail will not create any new threads.", - color=self.bot.main_color, + description=f"Modmail is not creating new threads.", + color=self.bot.error_color, ) elif self.bot.config["dm_disabled"] == 2: embed = discord.Embed( title="All DM Disabled", - description=f"Modmail will accept any DM messages for new and existing threads.", - color=self.bot.main_color, + description=f"Modmail is not accepting any DM messages for new and existing threads.", + color=self.bot.error_color, ) else: embed = discord.Embed( title="Enabled", - description=f"Modmail is receiving all DM messages.", + description=f"Modmail is accepting all DM messages.", color=self.bot.main_color, ) From 5ed556d1bd30d87c27a50ae891dee99ca4d4d802 Mon Sep 17 00:00:00 2001 From: Robin Mahieu <42642013+papiersnipper@users.noreply.github.com> Date: Sat, 2 Nov 2019 20:11:19 +0100 Subject: [PATCH 46/48] Delete notes, "?logs delete", linked alias (#402) * Add ability to delete notes * Add wipe command * Add option to add existing aliases in new aliases * Fix typo in presence docstring * Update changelog and bump dev version --- .lint.py | 2 +- CHANGELOG.md | 5 +++- bot.py | 2 +- cogs/modmail.py | 29 ++++++++++++++++++++++- cogs/plugins.py | 2 +- cogs/utility.py | 62 ++++++++++++++++++++++++++++++++----------------- core/clients.py | 8 ++++--- core/thread.py | 1 - 8 files changed, 81 insertions(+), 30 deletions(-) diff --git a/.lint.py b/.lint.py index fd3cbc2f19..9d29372fd8 100644 --- a/.lint.py +++ b/.lint.py @@ -1,4 +1,4 @@ -if __name__ == '__main__': +if __name__ == "__main__": import sys from os import listdir from os.path import join diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e8313c5a..cb7e092741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev5 +# v3.3.0-dev6 ### Important @@ -39,6 +39,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - `disabled_current_thread_title` - `disabled_current_thread_response` - `disabled_current_thread_footer` +- Ability to delete notes when providing their ID. (Thanks to papiersnipper PR#402) +- Ability to delete log entries. (Thanks to papiersnipper PR#402) ### Changed @@ -62,6 +64,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Eval commands are logged in debug logs. - Presence updates 30 minutes instead of 45 now. - Fixed an assortment of problems to do with block. +- Existing aliases can be used when creating new aliases. (Thanks to papiersnipper PR#402) ### Internal diff --git a/bot.py b/bot.py index 34df9f1bb5..729722094f 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev5" +__version__ = "3.3.0-dev6" import asyncio import logging diff --git a/cogs/modmail.py b/cogs/modmail.py index 62f36ac6dd..b8215073df 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -722,6 +722,31 @@ async def logs_closed_by(self, ctx, *, user: User = None): session = EmbedPaginatorSession(ctx, *embeds) await session.run() + @logs.command(name="delete", aliases=["wipe"]) + @checks.has_permissions(PermissionLevel.OWNER) + async def logs_delete(self, ctx, key_or_link: str): + """ + Wipe a log entry from the database. + """ + key = key_or_link.split("/")[-1] + + success = await self.bot.api.delete_log_entry(key) + + if not success: + embed = discord.Embed( + title="Error", + description=f"Log entry `{key}` not found.", + color=self.bot.error_color, + ) + else: + embed = discord.Embed( + title="Success", + description=f"Log entry `{key}` successfully deleted.", + color=self.bot.main_color, + ) + + await ctx.send(embed=embed) + @logs.command(name="responded") @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_responded(self, ctx, *, user: User = None): @@ -1167,10 +1192,12 @@ async def unblock(self, ctx, *, user: User = None): @checks.thread_only() async def delete(self, ctx, message_id: Optional[int] = None): """ - Delete a message that was sent using the reply command. + Delete a message that was sent using the reply command or a note. Deletes the previous message, unless a message ID is provided, which in that case, deletes the message with that message ID. + + Notes can only be deleted when a note ID is provided. """ thread = ctx.thread diff --git a/cogs/plugins.py b/cogs/plugins.py index 52fc20bdd6..b9efd78d9b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -226,7 +226,7 @@ async def load_plugin(self, plugin): try: self.bot.load_extension(plugin.ext_string) - logger.info("Loaded plugin: %s", plugin.ext_string.split('.')[-1]) + logger.info("Loaded plugin: %s", plugin.ext_string.split(".")[-1]) self.loaded_plugins.add(plugin) except commands.ExtensionError as exc: diff --git a/cogs/utility.py b/cogs/utility.py index 57c536f4ea..51604472bf 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -305,7 +305,12 @@ async def about(self, ctx): latest = changelog.latest_version if self.bot.version.is_prerelease: - stable = next(filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions)) + stable = next( + filter( + lambda v: not parse_version(v.version).is_prerelease, + changelog.versions, + ) + ) footer = f"You are on the prerelease version • the latest version is v{stable.version}." elif self.bot.version < parse_version(latest.version): footer = f"A newer version is available v{latest.version}." @@ -509,7 +514,9 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) - activity, _ = await self.set_presence(activity_type=activity_type, activity_message=message) + activity, _ = await self.set_presence( + activity_type=activity_type, activity_message=message + ) self.bot.config["activity_type"] = activity.type.value self.bot.config["activity_message"] = activity.name @@ -565,7 +572,9 @@ async def status(self, ctx, *, status_type: str.lower): ) return await ctx.send(embed=embed) - async def set_presence(self, *, status=None, activity_type=None, activity_message=None): + async def set_presence( + self, *, status=None, activity_type=None, activity_message=None + ): if status is None: status = self.bot.config.get("status") @@ -574,9 +583,13 @@ async def set_presence(self, *, status=None, activity_type=None, activity_messag activity_type = self.bot.config.get("activity_type") url = None - activity_message = (activity_message or self.bot.config["activity_message"]).strip() + activity_message = ( + activity_message or self.bot.config["activity_message"] + ).strip() if activity_type is not None and not activity_message: - logger.warning("No activity message found whilst activity is provided, defaults to \"Modmail\".") + logger.warning( + 'No activity message found whilst activity is provided, defaults to "Modmail".' + ) activity_message = "Modmail" if activity_type == ActivityType.listening: @@ -599,14 +612,14 @@ async def set_presence(self, *, status=None, activity_type=None, activity_messag @tasks.loop(minutes=30) async def loop_presence(self): - """Set presence to the configured value every 45 minutes.""" + """Set presence to the configured value every 30 minutes.""" logger.debug("Resetting presence.") await self.set_presence() @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() - logger.line('debug') + logger.line("debug") activity, status = await self.set_presence() if activity is not None: @@ -1044,13 +1057,16 @@ async def alias_add(self, ctx, name: str.lower, *, value): if len(values) == 1: linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description="The command you are attempting to point " - f"to does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) + if linked_command in self.bot.aliases: + values = [self.bot.aliases.get(linked_command)] + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="The command you are attempting to point " + f"to does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) embed = discord.Embed( title="Added alias", @@ -1068,13 +1084,17 @@ async def alias_add(self, ctx, name: str.lower, *, value): for i, val in enumerate(values, start=1): linked_command = val.split()[0] if not self.bot.get_command(linked_command): - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description="The command you are attempting to point " - f"to on step {i} does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) + if linked_command in self.bot.aliases: + index = values.index(linked_command) + values[index] = self.bot.aliases.get(linked_command) + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="The command you are attempting to point " + f"to on step {i} does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) embed.description += f"\n{i}: {val}" self.bot.aliases[name] = " && ".join(values) diff --git a/core/clients.py b/core/clients.py index f01753bf23..486f792744 100644 --- a/core/clients.py +++ b/core/clients.py @@ -106,9 +106,7 @@ async def get_responded_logs(self, user_id: Union[str, int]) -> list: return await self.logs.find(query).to_list(None) async def get_open_logs(self) -> list: - query = { - "open": True - } + query = {"open": True} return await self.logs.find(query).to_list(None) async def get_log(self, channel_id: Union[str, int]) -> dict: @@ -162,6 +160,10 @@ async def create_log_entry( prefix = "" return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{key}" + async def delete_log_entry(self, key: str) -> bool: + result = await self.logs.delete_one({"key": key}) + return result.deleted_count == 1 + async def get_config(self) -> dict: conf = await self.db.config.find_one({"bot_id": self.bot.user.id}) if conf is None: diff --git a/core/thread.py b/core/thread.py index 01523e7d13..65147b4a6f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -439,7 +439,6 @@ async def _fetch_timeout( """ This grabs the timeout value for closing threads automatically from the ConfigManager and parses it for use internally. - :returns: None if no timeout is set. """ timeout = self.bot.config.get("thread_auto_close") From e87ca0ed9b65ed11e6cd9938ea7bbd3a74752fd0 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 2 Nov 2019 12:13:19 -0700 Subject: [PATCH 47/48] Keep arugments for linked alias --- cogs/utility.py | 69 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 51604472bf..0e29a5fd23 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1055,10 +1055,14 @@ async def alias_add(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) if len(values) == 1: - linked_command = values[0].split()[0].lower() + linked_command, *messages = values[0].split(maxsplit=1) if not self.bot.get_command(linked_command): - if linked_command in self.bot.aliases: - values = [self.bot.aliases.get(linked_command)] + alias_command = self.bot.aliases.get(linked_command) + if alias_command is not None: + if messages: + values = [f"{alias_command} {messages[0]}"] + else: + values = [alias_command] else: embed = discord.Embed( title="Error", @@ -1082,17 +1086,20 @@ async def alias_add(self, ctx, name: str.lower, *, value): ) for i, val in enumerate(values, start=1): - linked_command = val.split()[0] + linked_command, *messages = val.split(maxsplit=1) if not self.bot.get_command(linked_command): - if linked_command in self.bot.aliases: - index = values.index(linked_command) - values[index] = self.bot.aliases.get(linked_command) + alias_command = self.bot.aliases.get(linked_command) + if alias_command is not None: + if messages: + values = [f"{alias_command} {messages[0]}"] + else: + values = [alias_command] else: embed = discord.Embed( title="Error", color=self.bot.error_color, description="The command you are attempting to point " - f"to on step {i} does not exist: `{linked_command}`.", + f"to n step {i} does not exist: `{linked_command}`.", ) return await ctx.send(embed=embed) embed.description += f"\n{i}: {val}" @@ -1143,15 +1150,22 @@ async def alias_edit(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) if len(values) == 1: - linked_command = values[0].split()[0].lower() + linked_command, *messages = values[0].split(maxsplit=1) if not self.bot.get_command(linked_command): - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description="The command you are attempting to point " - f"to does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) + alias_command = self.bot.aliases.get(linked_command) + if alias_command is not None: + if messages: + values = [f"{alias_command} {messages[0]}"] + else: + values = [alias_command] + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="The command you are attempting to point " + f"to does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) embed = discord.Embed( title="Edited alias", color=self.bot.main_color, @@ -1166,15 +1180,22 @@ async def alias_edit(self, ctx, name: str.lower, *, value): ) for i, val in enumerate(values, start=1): - linked_command = val.split()[0] + linked_command, *messages = val.split(maxsplit=1) if not self.bot.get_command(linked_command): - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description="The command you are attempting to point " - f"to on step {i} does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) + alias_command = self.bot.aliases.get(linked_command) + if alias_command is not None: + if messages: + values = [f"{alias_command} {messages[0]}"] + else: + values = [alias_command] + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="The command you are attempting to point " + f"to on step {i} does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) embed.description += f"\n{i}: {val}" self.bot.aliases[name] = "&&".join(values) From c8f80397251f01cf2660e3a76fbe17c938008d6a Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 2 Nov 2019 21:15:12 -0700 Subject: [PATCH 48/48] Change logging --- bot.py | 51 ++++++++---------------------------------- cogs/modmail.py | 5 ++--- cogs/plugins.py | 5 ++--- cogs/utility.py | 7 +++--- core/changelog.py | 4 ++-- core/checks.py | 6 ++--- core/clients.py | 5 +++-- core/config.py | 5 ++--- core/models.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++ core/thread.py | 4 ++-- core/time.py | 5 +++-- 11 files changed, 86 insertions(+), 67 deletions(-) diff --git a/bot.py b/bot.py index 729722094f..da2801efb3 100644 --- a/bot.py +++ b/bot.py @@ -34,33 +34,12 @@ from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import human_join, parse_alias -from core.models import PermissionLevel, ModmailLogger, SafeFormatter +from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging from core.thread import ThreadManager from core.time import human_timedelta -logger: ModmailLogger = logging.getLogger("Modmail") -logger.__class__ = ModmailLogger - -logger.setLevel(logging.INFO) - -ch = logging.StreamHandler(stream=sys.stdout) -ch.setLevel(logging.INFO) -formatter = logging.Formatter( - "%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", - datefmt="%b %d %H:%M:%S", -) -ch.setFormatter(formatter) -logger.addHandler(ch) - - -class FileFormatter(logging.Formatter): - ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") - - def format(self, record): - record.msg = self.ansi_escape.sub("", record.msg) - return super().format(record) - +logger = getLogger(__name__) temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp") if not os.path.exists(temp_dir): @@ -125,7 +104,7 @@ def startup(self): logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") - logger.line("debug") + logger.line() for cog in self.loaded_cogs: logger.debug("Loading %s.", cog) @@ -145,30 +124,18 @@ def _configure_logging(self): "INFO": logging.INFO, "DEBUG": logging.DEBUG, } - - ch_debug = logging.FileHandler(self.log_file_name, mode="a+") - - ch_debug.setLevel(logging.DEBUG) - formatter_debug = FileFormatter( - "%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - ch_debug.setFormatter(formatter_debug) - logger.addHandler(ch_debug) + logger.line() log_level = logging_levels.get(level_text) if log_level is None: log_level = self.config.remove("log_level") - - logger.line() - if log_level is not None: - logger.setLevel(log_level) - ch.setLevel(log_level) - logger.info("Logging level: %s", level_text) + logger.warning("Invalid logging level set: %s.", level_text) + logger.warning("Using default logging level: INFO.") else: - logger.info("Invalid logging level set.") - logger.warning("Using default logging level: %s.", level_text) + logger.info("Logging level: %s", level_text) + logger.info("Log file: %s", self.log_file_name) + configure_logging(self.log_file_name, log_level) logger.debug("Successfully configured logging.") @property diff --git a/cogs/modmail.py b/cogs/modmail.py index b8215073df..8ce3f838fe 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,5 +1,4 @@ import asyncio -import logging from datetime import datetime from itertools import zip_longest from typing import Optional, Union @@ -13,7 +12,7 @@ from natural.date import duration from core import checks -from core.models import PermissionLevel +from core.models import PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta from core.utils import ( @@ -24,7 +23,7 @@ trigger_typing, ) -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class Modmail(commands.Cog): diff --git a/cogs/plugins.py b/cogs/plugins.py index b9efd78d9b..15db8214e4 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -1,7 +1,6 @@ import asyncio import io import json -import logging import os import shutil import sys @@ -20,11 +19,11 @@ from pkg_resources import parse_version from core import checks -from core.models import PermissionLevel +from core.models import PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession from core.utils import truncate, trigger_typing -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class InvalidPluginError(commands.BadArgument): diff --git a/cogs/utility.py b/cogs/utility.py index 0e29a5fd23..9610582334 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1,6 +1,5 @@ import asyncio import inspect -import logging import os import traceback import random @@ -24,11 +23,11 @@ from core import checks from core.changelog import Changelog -from core.models import InvalidConfigError, PermissionLevel +from core.models import InvalidConfigError, PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession, MessagePaginatorSession from core import utils -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class ModmailHelpCommand(commands.HelpCommand): @@ -619,7 +618,7 @@ async def loop_presence(self): @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() - logger.line("debug") + logger.line() activity, status = await self.set_presence() if activity is not None: diff --git a/core/changelog.py b/core/changelog.py index 1e9254286f..91856600e9 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -1,12 +1,12 @@ -import logging import re from typing import List from discord import Embed +from core.models import getLogger from core.utils import truncate -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class Version: diff --git a/core/checks.py b/core/checks.py index 6ccf1647b8..0a5a43e1bb 100644 --- a/core/checks.py +++ b/core/checks.py @@ -1,10 +1,8 @@ -import logging - from discord.ext import commands -from core.models import PermissionLevel +from core.models import PermissionLevel, getLogger -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) def has_permissions_predicate( diff --git a/core/clients.py b/core/clients.py index 486f792744..8d89331664 100644 --- a/core/clients.py +++ b/core/clients.py @@ -1,4 +1,3 @@ -import logging import secrets from datetime import datetime from json import JSONDecodeError @@ -8,7 +7,9 @@ from aiohttp import ClientResponseError, ClientResponse -logger = logging.getLogger("Modmail") +from core.models import getLogger + +logger = getLogger(__name__) class RequestClient: diff --git a/core/config.py b/core/config.py index 428e6005d3..bfc4a3b675 100644 --- a/core/config.py +++ b/core/config.py @@ -1,6 +1,5 @@ import asyncio import json -import logging import os import re import typing @@ -13,11 +12,11 @@ from discord.ext.commands import BadArgument from core._color_data import ALL_COLORS -from core.models import InvalidConfigError, Default +from core.models import InvalidConfigError, Default, getLogger from core.time import UserFriendlyTime from core.utils import strtobool -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) load_dotenv() diff --git a/core/models.py b/core/models.py index 47e6375cc5..5a1b455f61 100644 --- a/core/models.py +++ b/core/models.py @@ -1,5 +1,7 @@ import _string import logging +import re +import sys from enum import IntEnum from string import Formatter @@ -88,6 +90,60 @@ def line(self, level="info"): ) +logging.setLoggerClass(ModmailLogger) +log_level = logging.INFO +loggers = set() + +ch = logging.StreamHandler(stream=sys.stdout) +ch.setLevel(log_level) +formatter = logging.Formatter( + "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%m/%d/%y %H:%M:%S", +) +ch.setFormatter(formatter) + +ch_debug = None + + +def getLogger(name=None) -> ModmailLogger: + logger = logging.getLogger(name) + logger.setLevel(log_level) + logger.addHandler(ch) + if ch_debug is not None: + logger.addHandler(ch_debug) + loggers.add(logger) + return logger + + +class FileFormatter(logging.Formatter): + ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") + + def format(self, record): + record.msg = self.ansi_escape.sub("", record.msg) + return super().format(record) + + +def configure_logging(name, level=None): + global ch_debug, log_level + ch_debug = logging.FileHandler(name, mode="a+") + + formatter_debug = FileFormatter( + "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ch_debug.setFormatter(formatter_debug) + ch_debug.setLevel(logging.DEBUG) + + if level is not None: + log_level = level + + ch.setLevel(log_level) + + for logger in loggers: + logger.setLevel(log_level) + logger.addHandler(ch_debug) + + class _Default: pass diff --git a/core/thread.py b/core/thread.py index 65147b4a6f..03cefcae3d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,5 +1,4 @@ import asyncio -import logging import re import string import typing @@ -11,10 +10,11 @@ import discord from discord.ext.commands import MissingRequiredArgument, CommandError +from core.models import getLogger from core.time import human_timedelta from core.utils import is_image_url, days, match_user_id, truncate -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class Thread: diff --git a/core/time.py b/core/time.py index 5e8049567a..ba27fa021c 100644 --- a/core/time.py +++ b/core/time.py @@ -3,7 +3,6 @@ Source: https://github.com/Rapptz/RoboDanny/blob/rewrite/cogs/utils/time.py """ -import logging import re from datetime import datetime @@ -12,7 +11,9 @@ import parsedatetime as pdt from dateutil.relativedelta import relativedelta -logger = logging.getLogger("Modmail") +from core.models import getLogger + +logger = getLogger(__name__) class ShortTime: