From 9d4fb27ef296e38318e37b1a3b3a12a747612bd3 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 17 Aug 2019 00:19:40 -1000 Subject: [PATCH 01/17] v3.2.0 dev --- CHANGELOG.md | 18 ++++ README.md | 6 +- bot.py | 51 ++++++++-- cogs/modmail.py | 8 +- cogs/utility.py | 230 +++++++++++++++++++++++++++++++++++------- core/changelog.py | 60 ++++++----- core/checks.py | 58 ++++------- core/config.py | 1 + core/config_help.json | 8 +- core/utils.py | 16 +-- 10 files changed, 331 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fca34438db..09998321ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,28 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 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). +# v3.2.0 + +### Added + +- Ability to change permission levels of individual commands. + - See `?permissions override` for more information. + +### Fixed + +- `?help `, will return `Perhaps you meant: `, now its fixed. + - For example, `?help add` used to return `Perhaps you meant: add`, now it wouldn't do this. +- Aliases and Permissions command names are always saved lowercase now. + +### Internal + +- Use regex to parse Changes, Added, Fixed, etc and description. + # v3.1.1 ### Fixed diff --git a/README.md b/README.md index b43079e850..46e41697e9 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,9 @@ This list is ever-growing thanks to active development and our exceptional contr ## Installation -Where is the Modmail bot invite link? Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time): +Where can I find the Modmail bot invite link? + +Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time)... ### Heroku @@ -104,7 +106,7 @@ If you don't want to go through the trouble of setting up your very own Modmail ### Locally -Local hosting of Modmail is also possible, first you will need [`python 3.7`](https://www.python.org/downloads/). +Local hosting of Modmail is also possible, first you will need [`Python 3.7`](https://www.python.org/downloads/). Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. diff --git a/bot.py b/bot.py index 50b8351474..3bb205c991 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.1.1" +__version__ = "3.2.0" import asyncio import logging @@ -382,6 +382,34 @@ def recipient_color(self) -> int: def main_color(self) -> int: return self._parse_color("main_color") + def command_perm(self, command_name: str) -> PermissionLevel: + level = self.config["override_command_level"].get(command_name) + if level is not None: + try: + return PermissionLevel[level.upper()] + except KeyError: + logger.warning( + "Invalid override_command_level for command %s.", command_name + ) + self.config["override_command_level"].pop(command_name) + + command = self.get_command(command_name) + if command is None: + logger.debug("Command %s not found.", command_name) + return PermissionLevel.INVALID + level = next( + ( + check.permission_level + for check in command.checks + if hasattr(check, "permission_level") + ), + None, + ) + if level is None: + logger.debug("Command %s does not have a permission level.", command_name) + return PermissionLevel.INVALID + return level + async def on_connect(self): logger.line() try: @@ -1039,12 +1067,23 @@ async def on_command_error(self, context, exception): await context.send_help(context.command) elif isinstance(exception, commands.CheckFailure): for check in context.command.checks: - if not await check(context) and hasattr(check, "fail_msg"): - await context.send( - embed=discord.Embed( - color=discord.Color.red(), description=check.fail_msg + if not await check(context): + if hasattr(check, "fail_msg"): + await context.send( + embed=discord.Embed( + color=discord.Color.red(), description=check.fail_msg + ) + ) + if hasattr(check, 'permission_level'): + corrected_permission_level = self.command_perm( + context.command.qualified_name + ) + logger.warning( + "User %s does not have permission to use this command: `%s` (%s).", + context.author.name, + context.command.qualified_name, + corrected_permission_level.name, ) - ) logger.warning("CheckFailure: %s", exception) else: logger.error("Unexpected exception:", exc_info=exception) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7f2bb4ce56..e3f57ffb7d 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -912,10 +912,10 @@ async def blocked(self, ctx): line = mention + f" - `{reason or 'No reason provided'}`\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title="Blocked Users (Continued)", - color=self.bot.main_color, - description=line, - ) + title="Blocked Users (Continued)", + color=self.bot.main_color, + description=line, + ) embeds.append(embed) else: embed.description += line diff --git a/cogs/utility.py b/cogs/utility.py index 188da898bf..00963b88b2 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -41,9 +41,9 @@ 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=utils.get_perm_level, + key=lambda c: bot.command_perm(c.qualified_name), ): - perm_level = utils.get_perm_level(cmd) + perm_level = bot.command_perm(cmd.qualified_name) if perm_level is PermissionLevel.INVALID: format_ = f"`{prefix + cmd.qualified_name}` " else: @@ -122,11 +122,11 @@ async def send_cog_help(self, cog): async def send_command_help(self, command): if not await self.filter_commands([command]): return - perm_level = utils.get_perm_level(command) + perm_level = self.context.bot.command_perm(command.qualified_name) if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: - perm_level = "" + perm_level = "NONE" embed = Embed( title=f"`{self.get_command_signature(command)}`", @@ -140,11 +140,11 @@ async def send_group_help(self, group): if not await self.filter_commands([group]): return - perm_level = utils.get_perm_level(group) + 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 = "" + perm_level = "NONE" embed = Embed( title=f"`{self.get_command_signature(group)}`", @@ -153,7 +153,7 @@ async def send_group_help(self, group): ) if perm_level: - embed.add_field(name="Permission level", value=perm_level, inline=False) + embed.add_field(name="Permission Level", value=perm_level, inline=False) format_ = "" length = len(group.commands) @@ -169,7 +169,7 @@ async def send_group_help(self, group): branch = "├─" format_ += f"`{branch} {command.name}` - {command.short_doc}\n" - embed.add_field(name="Sub Commands", value=format_[:1024], inline=False) + embed.add_field(name="Sub Command(s)", value=format_[:1024], inline=False) embed.set_footer( text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' "for more info on a command." @@ -218,7 +218,7 @@ async def send_error_message(self, error): for cmd in self.context.bot.walk_commands(): if not cmd.hidden: - choices.add(cmd.name) + choices.add(cmd.qualified_name) closest = get_close_matches(command, choices) if closest: @@ -244,7 +244,7 @@ def __init__(self, bot): verify_checks=False, command_attrs={ "help": "Shows this help message.", - "checks": [checks.has_permissions_help(PermissionLevel.REGULAR)], + "checks": [checks.has_permissions_predicate(PermissionLevel.REGULAR)], }, ) self.bot.help_command.cog = self @@ -271,14 +271,21 @@ async def changelog(self, ctx, version: str.lower = ""): ) ) + paginator = EmbedPaginatorSession(ctx, *changelog.embeds) try: - paginator = EmbedPaginatorSession(ctx, *changelog.embeds) paginator.current = index await paginator.run() except asyncio.CancelledError: pass except Exception: - await ctx.send(changelog.CHANGELOG_URL) + 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.CHANGELOG_URL}#v{version[::2]}" + ) @commands.command(aliases=["bot", "info"]) @checks.has_permissions(PermissionLevel.REGULAR) @@ -640,14 +647,17 @@ async def set_presence( async def loop_presence(self): """Set presence to the configured value every 45 minutes.""" presence = await self.set_presence() - logger.line() - logger.info(presence["activity"][1]) - logger.info(presence["status"][1]) + logger.debug(f'{presence["activity"][1]} {presence["status"][1]}') @loop_presence.before_loop async def before_loop_presence(self): logger.info("Starting metadata loop.") 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) @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) @@ -1066,7 +1076,7 @@ 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] + linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): embed = Embed( title="Error", @@ -1147,7 +1157,7 @@ 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] + linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): embed = Embed( title="Error", @@ -1203,6 +1213,9 @@ async def permissions(self, ctx): By default, owner is set to the absolute bot owner and regular is `@everyone`. + To set permissions, see `{prefix}help permissions add`; and to change permission level for specific + commands see `{prefix}help permissions override`. + Note: You will still have to manually give/take permission to the Modmail category to users/roles. """ @@ -1233,9 +1246,64 @@ def _parse_level(name): "4": PermissionLevel.ADMINISTRATOR, "5": PermissionLevel.OWNER, } - return transform.get(name) + return transform.get(name, PermissionLevel.INVALID) + + @permissions.command(name="override") + @checks.has_permissions(PermissionLevel.OWNER) + async def permissions_override( + self, ctx, command_name: str.lower, *, level_name: str + ): + """ + Change a permission level for a specific command. + + Examples: + - `{prefix}perms override reply administrator` + - `{prefix}perms override "plugin enabled" moderator` + + To undo a permission override, see `{prefix}help permissions remove`. + + Example: + - `{prefix}perms remove override reply` + - `{prefix}perms remove override plugin enabled` + + You can retrieve a single or all command level override(s), see`{prefix}help permissions get`. + """ + + command = self.bot.get_command(command_name) + if command is None: + embed = Embed( + title="Error", + color=Color.red(), + 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( + title="Error", + color=Color.red(), + description=f"The referenced level does not exist: `{level_name}`.", + ) + else: + logger.info( + "Updated command permission level for `%s` to `%s`.", + command.qualified_name, + level.name, + ) + self.bot.config["override_command_level"][ + command.qualified_name + ] = level.name + await self.bot.config.update() + embed = Embed( + title="Success", + color=self.bot.main_color, + description="Successfully set command permission level for " + f"`{command.qualified_name}` to `{level.name}`.", + ) + return await ctx.send(embed=embed) - @permissions.command(name="add", usage="[command/level] [name] [user_or_role]") + @permissions.command(name="add", usage="[command/level] [name] [user/role]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add( self, @@ -1265,11 +1333,12 @@ async def permissions_add( command = level = None if type_ == "command": - command = self.bot.get_command(name.lower()) + name = name.lower() + command = self.bot.get_command(name) check = command is not None else: level = self._parse_level(name) - check = level is not None + check = level is not PermissionLevel.INVALID if not check: embed = Embed( @@ -1281,8 +1350,8 @@ async def permissions_add( value = self._verify_user_or_role(user_or_role) if type_ == "command": - await self.bot.update_perms(command.qualified_name, value) name = command.qualified_name + await self.bot.update_perms(name, value) else: await self.bot.update_perms(level, value) name = level.name @@ -1309,7 +1378,7 @@ async def permissions_add( @permissions.command( name="remove", aliases=["del", "delete", "revoke"], - usage="[command/level] [name] [user_or_role]", + usage="[command/level] [name] [user/role] or [override] [command name]", ) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove( @@ -1318,10 +1387,10 @@ async def permissions_remove( type_: str.lower, name: str, *, - user_or_role: Union[Role, utils.User, str], + user_or_role: Union[Role, utils.User, str] = None, ): """ - Remove permission to use a command or permission level. + Remove permission to use a command, permission level, or command level override. For sub commands, wrap the complete command name with quotes. To find a list of permission levels, see `{prefix}help perms`. @@ -1331,22 +1400,54 @@ async def permissions_remove( - `{prefix}perms remove command reply @user` - `{prefix}perms remove command "plugin enabled" @role` - `{prefix}perms remove command help 984301093849028` + - `{prefix}perms remove override block` + - `{prefix}perms remove override "snippet add"` Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, """ - if type_ not in {"command", "level"}: + if type_ not in {"command", "level", "override"} or ( + type_ != "override" and user_or_role is None + ): return await ctx.send_help(ctx.command) + if type_ == "override": + extension = ctx.kwargs["user_or_role"] + if extension is not None: + name += f" {extension}" + name = name.lower() + name = getattr(self.bot.get_command(name), "qualified_name", name) + level = self.bot.config["override_command_level"].get(name) + if level is None: + perm = self.bot.command_perm(name) + embed = Embed( + title="Error", + color=Color.red(), + description=f"The command permission level was never overridden: `{name}`, " + f"current permission level is {perm.name}.", + ) + else: + logger.info("Restored command permission level for `%s`.", name) + self.bot.config["override_command_level"].pop(name) + await self.bot.config.update() + perm = self.bot.command_perm(name) + embed = Embed( + title="Success", + color=self.bot.main_color, + description=f"Command permission level for `{name}` was successfully restored to {perm.name}.", + ) + return await ctx.send(embed=embed) + level = None if type_ == "command": - name = getattr(self.bot.get_command(name.lower()), "qualified_name", name) + name = name.lower() + name = getattr(self.bot.get_command(name), "qualified_name", name) else: level = self._parse_level(name) - if level is None: + if level is PermissionLevel.INVALID: embed = Embed( title="Error", color=Color.red(), - description=f"The referenced {type_} does not exist: `{name}`.", + description=f"The referenced level does not exist: `{name}`.", ) return await ctx.send(embed=embed) name = level.name @@ -1423,7 +1524,7 @@ def _get_perm(self, ctx, name, type_): ) return embed - @permissions.command(name="get", usage="[@user] or [command/level] [name]") + @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 @@ -1446,10 +1547,16 @@ async def permissions_get( - `{prefix}perms get command plugin remove` - `{prefix}perms get level SUPPORTER` + To view command level overrides: + + Examples: + - `{prefix}perms get override block` + - `{prefix}perms get override permissions add` + Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, """ - if name is None and user_or_role not in {"command", "level"}: + if name is None and user_or_role not in {"command", "level", "override"}: value = self._verify_user_or_role(user_or_role) cmds = [] @@ -1497,6 +1604,59 @@ async def permissions_get( ), ] else: + user_or_role = (user_or_role or "").lower() + if user_or_role == "override": + if name is None: + done = set() + + overrides = {} + for command in self.bot.walk_commands(): + if command not in done: + done.add(command) + level = self.bot.config["override_command_level"].get(command.qualified_name) + if level is not None: + overrides[command.qualified_name] = level + + embeds = [] + if not overrides: + embeds.append(Embed( + title="Permission Overrides", + description="You don't have any command level overrides at the moment.", + color=Color.red() + )) + else: + for items in zip_longest(*(iter(sorted(overrides.items())),) * 15): + description = "\n".join( + ": ".join((f"`{name}`", level)) + for name, level in takewhile(lambda x: x is not None, items) + ) + embed = Embed(color=self.bot.main_color, description=description) + embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon_url) + embeds.append(embed) + + session = EmbedPaginatorSession(ctx, *embeds) + return await session.run() + + name = name.lower() + name = getattr(self.bot.get_command(name), "qualified_name", name) + level = self.bot.config["override_command_level"].get(name) + perm = self.bot.command_perm(name) + if level is None: + embed = Embed( + title="Error", + color=Color.red(), + description=f"The command permission level was never overridden: `{name}`, " + f"current permission level is {perm.name}.", + ) + else: + embed = Embed( + title="Success", + color=self.bot.main_color, + description=f'Permission override for command "{name}" is "{perm.name}".', + ) + + return await ctx.send(embed=embed) + if user_or_role not in {"command", "level"}: return await ctx.send_help(ctx.command) embeds = [] @@ -1504,11 +1664,12 @@ async def permissions_get( name = name.strip('"') command = level = None if user_or_role == "command": - command = self.bot.get_command(name.lower()) + name = name.lower() + command = self.bot.get_command(name) check = command is not None else: level = self._parse_level(name) - check = level is not None + check = level is not PermissionLevel.INVALID if not check: embed = Embed( @@ -1543,7 +1704,8 @@ async def permissions_get( @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): - """Commands relating to logviewer oauth2 login authentication. + """ + Commands relating to logviewer oauth2 login authentication. This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature. """ diff --git a/core/changelog.py b/core/changelog.py index 386baf176f..3bf4ad865b 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -1,9 +1,10 @@ import logging import re -from collections import defaultdict -from typing import List, Union +from typing import List -from discord import Embed, Color +from discord import Embed + +from core.utils import truncate logger = logging.getLogger("Modmail") @@ -27,22 +28,29 @@ class Version: The Modmail bot. version : str The version string (ie. "v2.12.0"). - lines : List[str] + lines : str A list of lines of changelog messages for this version. - fields : defaultdict[str, str] + fields : Dict[str, str] A dict of fields separated by "Fixed", "Changed", etc sections. description : str General description of the version. + + Class Attributes + ---------------- + ACTION_REGEX : str + The regex used to parse the actions. + DESCRIPTION_REGEX: str + The regex used to parse the description. """ - def __init__(self, bot, version: str, lines: Union[List[str], str]): + ACTION_REGEX = r"###\s*(.+?)\s*\n(.*?)(?=###\s*.+?|$)" + DESCRIPTION_REGEX = r"^(.*?)(?=###\s*.+?|$)" + + def __init__(self, bot, version: str, lines: str): self.bot = bot self.version = version.lstrip("vV") - if isinstance(lines, list): - self.lines = lines - else: - self.lines = [x for x in lines.splitlines() if x] - self.fields = defaultdict(str) + self.lines = lines.strip() + self.fields = {} self.description = "" self.parse() @@ -53,26 +61,32 @@ def parse(self) -> None: """ Parse the lines and split them into `description` and `fields`. """ - curr_action = None + self.description = re.match(self.DESCRIPTION_REGEX, self.lines, re.DOTALL) + self.description = ( + self.description.group(1).strip() if self.description is not None else "" + ) - for line in self.lines: - if line.startswith("### "): - curr_action = line[4:] - elif curr_action is None: - self.description += line + "\n" - else: - self.fields[curr_action] += line + "\n" + matches = re.finditer(self.ACTION_REGEX, self.lines, re.DOTALL) + for m in matches: + try: + self.fields[m.group(1).strip()] = m.group(2).strip() + except AttributeError: + logger.error( + "Something went wrong when parsing the changelog for version %s.", + self.version, + exc_info=True, + ) @property def url(self) -> str: - return Changelog.CHANGELOG_URL + "#v" + self.version.replace(".", "") + return f"{Changelog.CHANGELOG_URL}#v{self.version[::2]}" @property def embed(self) -> Embed: """ Embed: the formatted `Embed` of this `Version`. """ - embed = Embed(color=Color.green(), description=self.description) + embed = Embed(color=self.bot.main_color, description=self.description) embed.set_author( name=f"v{self.version} - Changelog", icon_url=self.bot.user.avatar_url, @@ -80,7 +94,7 @@ def embed(self) -> Embed: ) for name, value in self.fields.items(): - embed.add_field(name=name, value=value) + embed.add_field(name=name, value=truncate(value, 1024)) embed.set_footer(text=f"Current version: v{self.bot.version}") embed.set_thumbnail(url=self.bot.user.avatar_url) return embed @@ -121,7 +135,7 @@ class Changelog: ) CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md" VERSION_REGEX = re.compile( - r"# ([vV]\d+\.\d+(?:\.\d+)?)(.*?(?=# (?:[vV]\d+\.\d+(?:\.\d+)?)|$))", + r"#\s*([vV]\d+\.\d+(?:\.\d+)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)?|$)", flags=re.DOTALL, ) diff --git a/core/checks.py b/core/checks.py index 17d2b4353b..2e3dbe58d9 100644 --- a/core/checks.py +++ b/core/checks.py @@ -7,11 +7,9 @@ logger = logging.getLogger("Modmail") -def has_permissions_help(permission_level: PermissionLevel = PermissionLevel.REGULAR): +def has_permissions_predicate(permission_level: PermissionLevel = PermissionLevel.REGULAR): async def predicate(ctx): - return await check_permissions( - ctx, ctx.command.qualified_name, permission_level - ) + return await check_permissions(ctx, ctx.command.qualified_name) predicate.permission_level = permission_level return predicate @@ -36,62 +34,46 @@ async def setup(ctx): print("Success") """ - async def predicate(ctx): - has_perm = await check_permissions( - ctx, ctx.command.qualified_name, permission_level - ) - - if not has_perm and ctx.command.qualified_name != "help": - logger.error( - "You do not have permission to use this command: `%s` (%s).", - str(ctx.command.qualified_name), - str(permission_level.name), - ) - return has_perm + return commands.check(has_permissions_predicate(permission_level)) - predicate.permission_level = permission_level - return commands.check(predicate) - -async def check_permissions( # pylint: disable=too-many-return-statements - ctx, command_name, permission_level -) -> bool: +async def check_permissions(ctx, command_name) -> bool: """Logic for checking permissions for a command for a user""" if await ctx.bot.is_owner(ctx.author): - # Direct bot owner (creator) has absolute power over the bot + # Bot owner(s) (and creator) has absolute power over the bot + return True + + permission_level = ctx.bot.command_perm(command_name) + + if permission_level is PermissionLevel.INVALID: + logger.warning("Invalid permission level for command %s.", command_name) return True if ( - permission_level != PermissionLevel.OWNER + permission_level is not PermissionLevel.OWNER and ctx.channel.permissions_for(ctx.author).administrator ): # Administrators have permission to all non-owner commands + logger.debug("Allowed due to administrator.") return True command_permissions = ctx.bot.config["command_permissions"] - author_roles = ctx.author.roles + checkables = {*ctx.author.roles, ctx.author} if command_name in command_permissions: # -1 is for @everyone - if -1 in command_permissions[command_name]: - return True - has_perm_role = any( - role.id in command_permissions[command_name] for role in author_roles + return -1 in command_permissions[command_name] or any( + check.id in command_permissions[command_name] for check in checkables ) - has_perm_id = ctx.author.id in command_permissions[command_name] - return has_perm_role or has_perm_id level_permissions = ctx.bot.config["level_permissions"] for level in PermissionLevel: if level >= permission_level and level.name in level_permissions: - if -1 in level_permissions[level.name]: - return True - has_perm_role = any( - role.id in level_permissions[level.name] for role in author_roles - ) - has_perm_id = ctx.author.id in level_permissions[level.name] - if has_perm_role or has_perm_id: + # -1 is for @everyone + if -1 in level_permissions[level.name] or any( + check.id in level_permissions[level.name] for check in checkables + ): return True return False diff --git a/core/config.py b/core/config.py index b682f32e04..d07b87ea3b 100644 --- a/core/config.py +++ b/core/config.py @@ -74,6 +74,7 @@ class ConfigManager: "blocked_whitelist": [], "command_permissions": {}, "level_permissions": {}, + "override_command_level": {}, # threads "snippets": {}, "notification_squad": {}, diff --git a/core/config_help.json b/core/config_help.json index be499bb230..2d9a9d68e2 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -412,7 +412,7 @@ }, "mongo_uri": { "default": "None, required", - "description": "A MongoDB SRV connection string.", + "description": "A MongoDB connection string.", "examples": [ ], "notes": [ @@ -425,7 +425,7 @@ "examples": [ ], "notes": [ - "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables." + "This configuration can only to be set through `.env` file or environment (config) variables." ] }, "token": { @@ -434,7 +434,7 @@ "examples": [ ], "notes": [ - "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables." + "This configuration can only to be set through `.env` file or environment (config) variables." ] }, "log_level": { @@ -443,7 +443,7 @@ "examples": [ ], "notes": [ - "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables." + "This configuration can only to be set through `.env` file or environment (config) variables." ] } } \ No newline at end of file diff --git a/core/utils.py b/core/utils.py index 585c0fba3d..de17e18419 100644 --- a/core/utils.py +++ b/core/utils.py @@ -55,6 +55,7 @@ def truncate(text: str, max: int = 50) -> str: # pylint: disable=redefined-buil str The truncated text. """ + text = text.strip() return text[: max - 3].strip() + "..." if len(text) > max else text @@ -75,7 +76,7 @@ def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]): messages = messages[:3] out = "" for message in messages: - if message.get("type") in ("note", "internal"): + if message.get("type") in {"note", "internal"}: continue author = message["author"] content = str(message["content"]).replace("\n", " ") @@ -193,19 +194,6 @@ def match_user_id(text: str) -> int: return -1 -def get_perm_level(cmd): - from core.models import PermissionLevel - - for check in cmd.checks: - perm = getattr(check, "permission_level", None) - if perm is not None: - return perm - for check in cmd.checks: - if "is_owner" in str(check): - return PermissionLevel.OWNER - return PermissionLevel.INVALID - - async def ignore(coro): try: await coro From 259ba09954aeda73dc5d4eef7727c7c75f1ff2e5 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 17 Aug 2019 10:43:08 -1000 Subject: [PATCH 02/17] Add checks when changing perm level --- cogs/utility.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cogs/utility.py b/cogs/utility.py index 00963b88b2..f3a543336e 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1294,6 +1294,11 @@ async def permissions_override( self.bot.config["override_command_level"][ command.qualified_name ] = level.name + + if not any(check for check in command.checks if hasattr(check, "permission_level")): + logger.debug('Command %s has no permissions check, adding invalid.', command.qualified_name) + checks.has_permissions(PermissionLevel.INVALID)(command) + await self.bot.config.update() embed = Embed( title="Success", From 0032a9dda426db2e3e586ab04ce3d5fb2cc38112 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 23 Aug 2019 02:23:50 -0700 Subject: [PATCH 03/17] Fix override --- CHANGELOG.md | 1 + bot.py | 14 +++++++++++++- cogs/utility.py | 37 +++++++++++++++++++++++-------------- core/checks.py | 4 +++- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09998321ce..8d89d2d73b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ however, insignificant breaking changes does not guarantee a major version bump, ### Internal - Use regex to parse Changes, Added, Fixed, etc and description. +- Adds `PermissionLevel.INVALID` when commands doesn't have a permission level. # v3.1.1 diff --git a/bot.py b/bot.py index 3bb205c991..62803d525d 100644 --- a/bot.py +++ b/bot.py @@ -29,6 +29,7 @@ except ImportError: pass +from core import checks from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import human_join, strtobool, parse_alias @@ -840,6 +841,17 @@ async def process_commands(self, message): ctxs = await self.get_contexts(message) for ctx in ctxs: if ctx.command: + if not any( + 1 + for check in ctx.command.checks + if hasattr(check, "permission_level") + ): + logger.debug( + "Command %s has no permissions check, adding invalid level.", + ctx.command.qualified_name, + ) + checks.has_permissions(PermissionLevel.INVALID)(ctx.command) + await self.invoke(ctx) continue @@ -1074,7 +1086,7 @@ async def on_command_error(self, context, exception): color=discord.Color.red(), description=check.fail_msg ) ) - if hasattr(check, 'permission_level'): + if hasattr(check, "permission_level"): corrected_permission_level = self.command_perm( context.command.qualified_name ) diff --git a/cogs/utility.py b/cogs/utility.py index f3a543336e..c10f845e72 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -646,6 +646,7 @@ async def set_presence( @tasks.loop(minutes=45) 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]}') @@ -1295,10 +1296,6 @@ async def permissions_override( command.qualified_name ] = level.name - if not any(check for check in command.checks if hasattr(check, "permission_level")): - logger.debug('Command %s has no permissions check, adding invalid.', command.qualified_name) - checks.has_permissions(PermissionLevel.INVALID)(command) - await self.bot.config.update() embed = Embed( title="Success", @@ -1618,25 +1615,37 @@ async def permissions_get( for command in self.bot.walk_commands(): if command not in done: done.add(command) - level = self.bot.config["override_command_level"].get(command.qualified_name) + level = self.bot.config["override_command_level"].get( + command.qualified_name + ) if level is not None: overrides[command.qualified_name] = level embeds = [] if not overrides: - embeds.append(Embed( - title="Permission Overrides", - description="You don't have any command level overrides at the moment.", - color=Color.red() - )) + embeds.append( + Embed( + title="Permission Overrides", + description="You don't have any command level overrides at the moment.", + color=Color.red(), + ) + ) else: - for items in zip_longest(*(iter(sorted(overrides.items())),) * 15): + for items in zip_longest( + *(iter(sorted(overrides.items())),) * 15 + ): description = "\n".join( ": ".join((f"`{name}`", level)) - for name, level in takewhile(lambda x: x is not None, items) + for name, level in takewhile( + lambda x: x is not None, items + ) + ) + embed = Embed( + color=self.bot.main_color, description=description + ) + embed.set_author( + name="Permission Overrides", icon_url=ctx.guild.icon_url ) - embed = Embed(color=self.bot.main_color, description=description) - embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon_url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) diff --git a/core/checks.py b/core/checks.py index 2e3dbe58d9..6ccf1647b8 100644 --- a/core/checks.py +++ b/core/checks.py @@ -7,7 +7,9 @@ logger = logging.getLogger("Modmail") -def has_permissions_predicate(permission_level: PermissionLevel = PermissionLevel.REGULAR): +def has_permissions_predicate( + permission_level: PermissionLevel = PermissionLevel.REGULAR +): async def predicate(ctx): return await check_permissions(ctx, ctx.command.qualified_name) From 244dd9490d1ee73c6987b4830d9f3ce2116cf293 Mon Sep 17 00:00:00 2001 From: weirdllamas Date: Fri, 23 Aug 2019 20:18:57 -0700 Subject: [PATCH 04/17] Added to Config: thread_move_notify and thread_move_response Closes Issue #189 --- cogs/modmail.py | 7 +++++++ core/config.py | 3 +++ core/config_help.json | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/cogs/modmail.py b/cogs/modmail.py index e3f57ffb7d..78f847d7f9 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -294,6 +294,13 @@ async def move(self, ctx, *, category: discord.CategoryChannel): """ thread = ctx.thread await thread.channel.edit(category=category, sync_permissions=True) + if self.bot.config["thread_move_notify"]: + embed = discord.Embed( + title="Thread Moved", + description=self.bot.config["thread_move_response"], + color=discord.Color.red()) + + await thread.recipient.send(embed=embed) sent_emoji, _ = await self.bot.retrieve_emoji() try: await ctx.message.add_reaction(sent_emoji) diff --git a/core/config.py b/core/config.py index d07b87ea3b..b2109a94da 100644 --- a/core/config.py +++ b/core/config.py @@ -53,6 +53,8 @@ class ConfigManager: "thread_close_title": "Thread Closed", "thread_close_response": "{closer.mention} has closed this Modmail thread.", "thread_self_close_response": "You have closed this Modmail thread.", + "thread_move_notify": False, + "thread_move_response": "This thread has been moved.", # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -109,6 +111,7 @@ class ConfigManager: "reply_without_command", "recipient_thread_close", "thread_auto_close_silently", + "thread_move_notify", } defaults = {**public_keys, **private_keys, **protected_keys} diff --git a/core/config_help.json b/core/config_help.json index 2d9a9d68e2..fd8649859c 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -300,6 +300,27 @@ "See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`." ] }, + "thread_move_notify": { + "default": "No", + "description": "Notify the recipient if the thread was moved.", + "examples": [ + "`{prefix}config set thread_move_notify yes`", + "`{prefix}config set thread_move_notify no`" + ], + "notes": [ + "See also: `thread_move_response`" + ] + }, + "thread_move_response": { + "default": "This thread has been moved.", + "description": "This is the message to display to the user when the thread is moved.", + "examples": [ + "`{prefix}config set thread_move_response This thread has been moved to another category for review!`" + ], + "notes": [ + "Only has an effect when thread_move_notify is on." + ] + }, "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.", From 8c5ff588812b3ce31f4b01b169fb295167e34b69 Mon Sep 17 00:00:00 2001 From: weirdllamas Date: Fri, 23 Aug 2019 20:25:41 -0700 Subject: [PATCH 05/17] Show Message ID Updated CHANGELOG.md Closes issue #344 --- CHANGELOG.md | 2 ++ core/thread.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09998321ce..c3aae3cefd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Ability to change permission levels of individual commands. - See `?permissions override` for more information. +- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved +- IDs of messages sent to Modmail are now viewable ### Fixed diff --git a/core/thread.py b/core/thread.py index 00aa24c9c7..b1ad46086d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -763,7 +763,7 @@ async def send( elif note: embed.colour = discord.Color.blurple() else: - embed.set_footer(text=f"Recipient") + embed.set_footer(text=f"Message ID: {message.id}") embed.colour = self.bot.recipient_color try: From 562214eea21e1065b930caccd9a84a1bdae0f0fb Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Sun, 25 Aug 2019 17:50:44 -0600 Subject: [PATCH 06/17] Update cogs/modmail.py Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> --- cogs/modmail.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 78f847d7f9..92e4b14c84 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -294,7 +294,11 @@ async def move(self, ctx, *, category: discord.CategoryChannel): """ thread = ctx.thread await thread.channel.edit(category=category, sync_permissions=True) - if self.bot.config["thread_move_notify"]: + try: + thread_move_notify = strtobool(self.config["thread_move_notify"]) + except ValueError: + thread_move_notify = self.config.remove("thread_move_notify") + if thread_move_notify: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], From 223c33f4148931e708cabb6ec4962f9a12fe25a8 Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Sun, 25 Aug 2019 17:51:16 -0600 Subject: [PATCH 07/17] Update core/config_help.json Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> --- core/config_help.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/config_help.json b/core/config_help.json index fd8649859c..44f211003b 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -308,7 +308,7 @@ "`{prefix}config set thread_move_notify no`" ], "notes": [ - "See also: `thread_move_response`" + "See also: `thread_move_response`." ] }, "thread_move_response": { @@ -467,4 +467,4 @@ "This configuration can only to be set through `.env` file or environment (config) variables." ] } -} \ No newline at end of file +} From bd6bb708d77b0f25772a128d47597f889efa512b Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Sun, 25 Aug 2019 17:51:26 -0600 Subject: [PATCH 08/17] Update core/config_help.json Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> --- core/config_help.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/config_help.json b/core/config_help.json index 44f211003b..50d0a9fad9 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -318,7 +318,8 @@ "`{prefix}config set thread_move_response This thread has been moved to another category for review!`" ], "notes": [ - "Only has an effect when thread_move_notify is on." + "Only has an effect when `thread_move_notify` is on.", + "See also: `thread_move_notify`." ] }, "recipient_color": { From 6bf83c8bee13ce4f70f7f651a711764a39d2b30b Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Sun, 25 Aug 2019 17:53:29 -0600 Subject: [PATCH 09/17] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3aae3cefd..4ba3ebf67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Ability to change permission levels of individual commands. - See `?permissions override` for more information. -- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved -- IDs of messages sent to Modmail are now viewable +- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved. +- IDs of messages sent to Modmail are now viewable. ### Fixed From 5f77f8e18f0f90ad861ee064b263bc2756028d40 Mon Sep 17 00:00:00 2001 From: weirdllamas Date: Sun, 25 Aug 2019 17:39:41 -0700 Subject: [PATCH 10/17] Updating modmail.move Added specifics to arguments of move, allowing for an easily expandable set of specifics for the command Added a specific for silent moving for modmail.move --- cogs/modmail.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 92e4b14c84..f390ace1c7 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 +from core.utils import format_preview, User, create_not_found_embed, format_description, strtobool logger = logging.getLogger("Modmail") @@ -286,25 +286,34 @@ 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): + async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None): """ Move a thread to another category. `category` may be a category ID, mention, or name. """ thread = ctx.thread + silent = False + + if specifics: + silent_words = ['silent', 'quiet'] + silent = any(word in specifics for word in silent_words) + await thread.channel.edit(category=category, sync_permissions=True) + try: - thread_move_notify = strtobool(self.config["thread_move_notify"]) + thread_move_notify = strtobool(self.bot.config["thread_move_notify"]) except ValueError: - thread_move_notify = self.config.remove("thread_move_notify") - if thread_move_notify: + thread_move_notify = self.bot.config.remove("thread_move_notify") + + if thread_move_notify and not silent: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], color=discord.Color.red()) await thread.recipient.send(embed=embed) + sent_emoji, _ = await self.bot.retrieve_emoji() try: await ctx.message.add_reaction(sent_emoji) From c32f44a2796e0aca54af60482c45c9edc3079710 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 25 Aug 2019 19:35:54 -0700 Subject: [PATCH 11/17] Updated docker --- .dockerignore | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 13 ++--- README.md | 35 ++++++++++++ 3 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..78f469063d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,154 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# PyCharm +.idea/ + +# MacOS +.DS_Store + +# VS Code +.vscode/ + +# Node +package-lock.json +node_modules/ + +# Modmail +config.json +plugins/ +!plugins/registry.json +temp/ +test.py + +# Other stuff +.env.example +.gitignore +.lint.py +.pylintrc +.travis.yml +app.json +CHANGELOG.md +CODE_OF_CONDUCT.md +CONTRIBUTING.md +Pipfile +Pipfile.lock +Procfile +pyproject.toml +README.md +runtime.txt +SPONSORS.json diff --git a/Dockerfile b/Dockerfile index 851c65b4cc..9735f92d08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ -FROM library/python:latest -RUN apt update && apt install -y pipenv -RUN mkdir -p /bot && cd /bot && git clone https://github.com/kyb3r/modmail . -WORKDIR /bot -RUN pipenv install - -CMD ["pipenv", "run", "bot"] \ No newline at end of file +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 +CMD ["python", "bot.py"] \ No newline at end of file diff --git a/README.md b/README.md index 46e41697e9..71b17e8e30 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,41 @@ Finally, start Modmail. $ pipenv run bot ``` +#### Docker + +This repo supplies a Dockerfile for simplified deployment. + +You can build your own Docker image: + +```console +$ docker build . --tag=modmail +``` + +or run directly from a pre-built version from https://hub.docker.com/. Currently there are two community release of Modmail: + +- Kyber's: + +```console +$ docker pull kyb3rr/modmail +``` + +- Taku's: + +```console +$ docker pull taaku18/modmail +# You can also choose one of the following: +$ docker pull taaku18/modmail:dev +$ docker pull taaku18/modmail: ( ex: 3.2.0, 3.2, etc.) +``` + +And to run your docker image: + +```console +$ docker run --env-file .env user/modmail +``` +- Replace `user/modmail` with `kyb3rr/modmail`, `taaku18/modmail`, `taaku18/modmail:3.2`, etc as above. +- `.env` should be the path to your env file, you can also supply a path: `/path/to/.env`. + ## Sponsors Special thanks to our sponsors for supporting the project. From 2e53b7c7c38727fcb4bbded3fb1ac50c8dff9757 Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Mon, 26 Aug 2019 14:30:02 -0600 Subject: [PATCH 12/17] Update modmail.py --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index f390ace1c7..42ce97f5a3 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -310,7 +310,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=discord.Color.red()) + color=self.bot.main_color await thread.recipient.send(embed=embed) From 133a565ba76428b5d0db2f4134064cd4dedc2cbb Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Mon, 26 Aug 2019 14:55:35 -0600 Subject: [PATCH 13/17] Update modmail.py --- cogs/modmail.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 42ce97f5a3..d63a9e8b59 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -291,13 +291,14 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = Move a thread to another category. `category` may be a category ID, mention, or name. + `specifics` is a string which takes in arguments on how to perform the move. Ex: "silently" """ thread = ctx.thread silent = False if specifics: - silent_words = ['silent', 'quiet'] - silent = any(word in specifics for word in silent_words) + silent_words = ['silent', 'silently'] + silent = any(word in silent_words for word in specifics.split()) await thread.channel.edit(category=category, sync_permissions=True) From d3bab1ac8caa7f75c8bcf3d96f823d2355a239d1 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 26 Aug 2019 14:12:15 -0700 Subject: [PATCH 14/17] Syntax error in previous commit --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index d63a9e8b59..a55b637ddb 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -312,7 +312,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = title="Thread Moved", description=self.bot.config["thread_move_response"], color=self.bot.main_color - + ) await thread.recipient.send(embed=embed) sent_emoji, _ = await self.bot.retrieve_emoji() From 872d3044a0f73ce59b0d5784799a9bf9cd9f2bce Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 27 Aug 2019 11:08:57 -0700 Subject: [PATCH 15/17] Add attribution --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83a9ab37e..6129498119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,20 +6,21 @@ 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). -# v3.2.0 +# UNRELEASED ### Added - Ability to change permission levels of individual commands. - See `?permissions override` for more information. -- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved. -- IDs of messages sent to Modmail are now viewable. +- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved. (Thanks to Flufster PR#360) +- IDs of messages sent to Modmail are now viewable. (Thanks to Flufster PR#360) ### Fixed - `?help `, will return `Perhaps you meant: `, now its fixed. - For example, `?help add` used to return `Perhaps you meant: add`, now it wouldn't do this. - Aliases and Permissions command names are always saved lowercase now. +- An improved Dockerfile. ### Internal From ff9554ed401ffb762dd742b39672d65abfd4ddd1 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Sep 2019 20:31:01 -0700 Subject: [PATCH 16/17] Merge --- .dockerignore | 1 + .gitignore | 1 + Pipfile | 2 +- Pipfile.lock | 132 +++++++++++++++++++++++------------------------- bot.py | 3 +- cogs/utility.py | 2 +- 6 files changed, 70 insertions(+), 71 deletions(-) diff --git a/.dockerignore b/.dockerignore index 78f469063d..57b6d8718c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -152,3 +152,4 @@ pyproject.toml README.md runtime.txt SPONSORS.json +stack.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8e80af7c26..a7fb960a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,4 @@ plugins/ !plugins/registry.json temp/ test.py +stack.yml \ No newline at end of file diff --git a/Pipfile b/Pipfile index 0b541bd697..cfb4f8ba59 100644 --- a/Pipfile +++ b/Pipfile @@ -19,7 +19,7 @@ dnspython = "~=1.16.0" parsedatetime = "==2.4" aiohttp = "<3.6.0,>=3.3.0" python-dotenv = ">=0.10.3" -pipenv = "==2018.11.26" +pipenv = "*" "discord.py" = "==1.2.3" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 5fbfe85392..ee4681eb19 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "46092f8b1032f2ac529f9b0df1aa725a4c6135d04826cd4cc4844af01879a4c2" + "sha256": "564773a120ff7ba8214e5efd98fab8daf98230eb4b27f8af2dd63afcad39994c" }, "pipfile-spec": 6, "requires": { @@ -97,11 +97,10 @@ }, "emoji": { "hashes": [ - "sha256:9ae01495fc3fcc04e9136ca1af8cae58726ec5dfaaa92f61f0732cbae9a12fa9", - "sha256:cecf8ed7cc535c999dc96e3fe7e7aacd176deb9ef62b3ef022c535e155084c44" + "sha256:b68112d40482a05e5da5d53da33d0aba3cce96891282c9c179cc340003c6c64e" ], "index": "pypi", - "version": "==0.5.2" + "version": "==0.5.3" }, "future": { "hashes": [ @@ -192,36 +191,36 @@ }, "pymongo": { "hashes": [ - "sha256:32421df60d06f479d71b6b539642e410ece3006e8910688e68df962c8eb40a21", - "sha256:324b22a8443e11faca44c96b20e7ec8a9e59a1e664457edeeb4f796080b31cde", - "sha256:4505ff8b7923dd7a8bed1bf25c9c4d0df5ab0b8b2821f2296533f2149a55f401", - "sha256:460b224681ea711e48e3638d15be2249024031b7dcb9622ba19c2e85bd5a26cc", - "sha256:47473b70c5f3cd5ddd2c49ab3b9ceafdafbbed5bc963f147df22a9343d7978f5", - "sha256:49375839af76834e9c5c3cc78c78386873fd0b2ad9a0860a7dc4ec9fe73af9dd", - "sha256:4a65f0f71ece86c860d30a1436b646db8ea32aec518845ef2903ca569faec32e", - "sha256:530621906c5dd6d27305b39c4e017701e5f4299aa68b93cde70eb985f94ca26f", - "sha256:54f4770b5810e8dc3cbeed675874195f02bb2bc4e95a9d665068edfb3baff4f7", - "sha256:5ed9382410e938b0ff76041c34018210504729a83bcf4f6a70c7092c28169f6f", - "sha256:61cad83637ae12c1c825130d7f9325cd6c162e3a64e8747a8144866020be3ff4", - "sha256:61e8e1c58b4fdf47ab79b7c7db8bb022c1e40b3b5fcbbaeea5fc94dc5c75638d", - "sha256:6e04e496af7d156b66cce70460011c621ecbadf5dcdce325c7acbb3cd6ea245d", - "sha256:7ef89ec435e89da902451dde6845066fe2770befaf0301fe2a1ac426b51fced3", - "sha256:854e8425e5eb775ccfffad04ecd094c99923d60a2c2d49babb5c435e836a91fa", - "sha256:9569796d48498e4db4e1d56284b626a8ed15f641ce3a8b2085f06bb03f4c2c88", - "sha256:9d50c99c6388863cbfdc5db9bad62e3a7c2e5fc151554a07c7f3c2530334a34f", - "sha256:9ea016c2c011df21f77c1f806ce45129a344ba2d414bd50f9e065b13a4a134be", - "sha256:a8421f0823174888fb12a5fa675322e756499d71e77ff712b4412d4b8f3c6503", - "sha256:aef7d88384ada699976350a285c7a333f96ebc959e98e7d2c98589f47bbf3b7f", - "sha256:b4d7ff9957ee770cf03bd7156a68a2f2e838e60712d9608eadc8741c15d01e72", - "sha256:c1db85c39e6a60588f855dbc7bd68fb0dab796096148ab5aa4abecaff19e1c6e", - "sha256:cee2fc0b94e66e7230da12fc4b3d34793c49957e16ee04f6468a94e264a1e41d", - "sha256:cf1dea28379a16b23e47db312883f07b3ba8d9d6abc1c59e51d4c8ae1820ab43", - "sha256:d1cd175df7c8b5fc976bade78bf4d9fb5aa7ab465c0f59931e380bbe188ef8fc", - "sha256:d48a94edf3cdd34524936a72ea01b352682b337f33a42db10ba29a96c37147d3", - "sha256:d9cc103a4e97f78bc77a1d72759ab3722f6cdf0374ad4fb4b0c53bd3238bdf98", - "sha256:fcb9ae8aa9158106c5d98a4349ec0d90b68f052d620b2d24622ba03b91e4d81d" - ], - "version": "==3.8.0" + "sha256:09f8196e1cb081713aa3face08d1806dc0a5dd64cb9f67fefc568519253a7ff2", + "sha256:1be549c0ce2ba8242c149156ae2064b12a5d4704448d49f630b4910606efd474", + "sha256:1f9fe869e289210250cba4ea20fbd169905b1793e1cd2737f423e107061afa98", + "sha256:3653cea82d1e35edd0a2355150daf8a27ebf12cf55182d5ad1046bfa288f5140", + "sha256:4249c6ba45587b959292a727532826c5032d59171f923f7f823788f413c2a5a3", + "sha256:4ff8f5e7c0a78983c1ee07894fff1b21c0e0ad3a122d9786cc3745fd60e4a2ce", + "sha256:56b29c638ab924716b48a3e94e3d7ac00b04acec1daa8190c36d61fc714c3629", + "sha256:56ec9358bbfe5ae3b25e785f8a14619d6799c855a44734c9098bb457174019bf", + "sha256:5dca250cbf1183c3e7b7b18c882c2b2199bfb20c74c4c68dbf11596808a296da", + "sha256:61101d1cc92881fac1f9ac7e99b033062f4c210178dc33193c8f5567feecb069", + "sha256:86624c0205a403fb4fbfedef79c5b4ab27e21fd018fdb6a27cf03b3c32a9e2b9", + "sha256:88ac09e1b197c3b4531e43054d49c022a3ea1281431b2f4980abafa35d2a5ce2", + "sha256:8b0339809b12ea292d468524dd1777f1a9637d9bdc0353a9261b88f82537d606", + "sha256:93dbf7388f6bf9af48dbb32f265b75b3dbc743a7a2ce98e44c88c049c58d85d3", + "sha256:9b705daec636c560dd2d63935f428a6b3cddfe903fffc0f349e0e91007c893d6", + "sha256:a090a819fe6fefadc2901d3911c07c76c0935ec5c790a50e9f3c3c47bacd5978", + "sha256:a102b346f1921237eaa9a31ee89eda57ad3c3973d79be3a456d92524e7df8fec", + "sha256:a13363869f2f36291d6367069c65d51d7b8d1b2fb410266b0b6b1f3c90d6deb0", + "sha256:a409a43c76da50881b70cc9ee70a1744f882848e8e93a68fb434254379777fa3", + "sha256:a76475834a978058425b0163f1bad35a5f70e45929a543075633c3fc1df564c5", + "sha256:ad474e93525baa6c58d75d63a73143af24c9f93c8e26e8d382f32c4da637901a", + "sha256:b268c7fa03ac77a8662fab3b2ab0be4beecb82f60f4c24b584e69565691a107f", + "sha256:cca4e1ab5ba0cd7877d3938167ee8ae9c2986cc0e10d3dcc3243d664d3a83fec", + "sha256:cef61de3f0f4441ec40266ff2ab42e5c16eaba1dc1fc6e1036f274621c52adc1", + "sha256:e28153b5d5ca33d4ba0c3bbc0e1ff161b9016e5e5f3f8ca10d6fa49106eb9e04", + "sha256:f30d7b37804daf0bab1143abc71666c630d7e270f5c14c5a7c300a6699c21108", + "sha256:f70f0133301cccf9bfd68fd20f67184ef991be578b646e78441106f9e27cc44d", + "sha256:fa75c21c1d82f20cce62f6fc4a68c2b0f33572ab406df1b17cd77a947d0b2993" + ], + "version": "==3.9.0" }, "python-dateutil": { "hashes": [ @@ -248,27 +247,24 @@ }, "uvloop": { "hashes": [ - "sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573", - "sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64", - "sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0", - "sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5", - "sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26", - "sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7", - "sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115", - "sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021", - "sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f", - "sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe" + "sha256:0deb6c97c5807c792dd9024bab90e6ca49e981862103cb2ea37b430c1ca0a267", + "sha256:155b34d513655e753d07f499a7e811970e2d397f240dfcbec0b32a9587159c99", + "sha256:1df3ddfa280206e9999ae1c777a20836eb895bcec6dc9fae2cbb6eecfafb099e", + "sha256:5b19361c8767e1dc61f6367f948d4f3dc5504b9f2eba488641b3d26ec14498ba", + "sha256:942cd07035510b149d6160796f4e972137130ae953871b6a98c2cf5d5ab68c2e", + "sha256:c63b6c0bf33144c604dd72f7eecf2d3a3ac7405c503c67bd98128cf306efe18a", + "sha256:e698a20a3b4ccb380d207f9d491d4085d7c38d364f6a0bae98684a1612a9607a" ], "index": "pypi", "markers": "sys_platform != 'win32'", - "version": "==0.12.2" + "version": "==0.13.0" }, "virtualenv": { "hashes": [ - "sha256:861bbce3a418110346c70f5c7a696fdcf23a261424e1d28aa4f9362fc2ccbc19", - "sha256:ba8ce6a961d842320681fb90a3d564d0e5134f41dacd0e2bae7f02441dde2d52" + "sha256:94a6898293d07f84a98add34c4df900f8ec64a570292279f6d91c781d37fd305", + "sha256:f6fc312c031f2d2344f885de114f1cb029dfcffd26aa6e57d2ee2296935c4e7d" ], - "version": "==16.6.2" + "version": "==16.7.4" }, "virtualenv-clone": { "hashes": [ @@ -366,26 +362,26 @@ }, "lazy-object-proxy": { "hashes": [ - "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", - "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", - "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", - "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", - "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", - "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", - "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", - "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", - "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", - "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", - "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", - "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", - "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", - "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", - "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", - "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", - "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", - "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1" - ], - "version": "==1.4.1" + "sha256:02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf", + "sha256:18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3", + "sha256:1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce", + "sha256:2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f", + "sha256:616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f", + "sha256:63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0", + "sha256:77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e", + "sha256:83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905", + "sha256:84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8", + "sha256:874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2", + "sha256:9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009", + "sha256:a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a", + "sha256:a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512", + "sha256:ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5", + "sha256:ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e", + "sha256:b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4", + "sha256:c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f", + "sha256:fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1" + ], + "version": "==1.4.2" }, "mccabe": { "hashes": [ diff --git a/bot.py b/bot.py index 62803d525d..bea7adadbb 100644 --- a/bot.py +++ b/bot.py @@ -1152,8 +1152,9 @@ async def post_metadata(self): logger.debug("Uploading metadata to Modmail server.") async def before_post_metadata(self): - logger.info("Starting metadata loop.") await self.wait_for_connected() + logger.debug("Starting metadata loop.") + logger.line() if not self.guild: self.metadata_loop.cancel() diff --git a/cogs/utility.py b/cogs/utility.py index c10f845e72..8f0986f06b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -652,8 +652,8 @@ async def loop_presence(self): @loop_presence.before_loop async def before_loop_presence(self): - logger.info("Starting metadata loop.") await self.bot.wait_for_connected() + logger.debug("Starting metadata loop.") logger.line() presence = await self.set_presence() logger.info(presence["activity"][1]) From cd063609edf21853e2208d712ea895e9f6c28d6c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Sep 2019 20:38:50 -0700 Subject: [PATCH 17/17] Bump version v3.2.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6129498119..bf0820128e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ 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 +# v3.2.0 ### Added