From 030f7b19e411c4e12da8b877f05558e0b979107c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 4 Nov 2019 01:10:41 -0800 Subject: [PATCH 01/38] strtobool support enable and disable, perhaps you meant for config help --- CHANGELOG.md | 7 +++++++ bot.py | 2 +- cogs/utility.py | 8 ++++++++ core/utils.py | 10 +++++++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d50213d4c..11b2959463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +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). +# v3.3.1-dev0 + +### Added + +- "enable" and "disable" support for yes or no config vars. +- Added "perhaps you meant" section to `?config help`. + # v3.3.0 diff --git a/bot.py b/bot.py index 8a0093246e..b9a663f910 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0" +__version__ = "3.3.1-dev0" import asyncio import logging diff --git a/cogs/utility.py b/cogs/utility.py index 9610582334..b540483174 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -848,11 +848,19 @@ 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 ): + closest = get_close_matches( + key, {**self.bot.config.public_keys, **self.bot.config.protected_keys} + ) embed = discord.Embed( title="Error", color=self.bot.error_color, description=f"`{key}` is an invalid key.", ) + if closest: + embed.add_field( + name=f"Perhaps you meant:", + value="\n".join(f"`{x}`" for x in closest), + ) return await ctx.send(embed=embed) config_help = self.bot.config.config_help diff --git a/core/utils.py b/core/utils.py index fd85f0d5b0..b62917119c 100644 --- a/core/utils.py +++ b/core/utils.py @@ -14,7 +14,15 @@ def strtobool(val): if isinstance(val, bool): return val - return _stb(str(val)) + try: + return _stb(str(val)) + except ValueError: + val = val.lower() + if val == "enable": + return 1 + elif val == "disable": + return 0 + raise class User(commands.IDConverter): From de71ab29307ffa7917ee4856bb23b8071359431c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 4 Nov 2019 02:12:44 -0800 Subject: [PATCH 02/38] Reformat code --- .pylintrc | 2 +- .travis.yml | 1 + CHANGELOG.md | 4 + bot.py | 93 +++++++--------------- cogs/modmail.py | 126 +++++++++--------------------- cogs/plugins.py | 75 ++++++------------ cogs/utility.py | 185 ++++++++++++-------------------------------- core/_color_data.py | 4 +- core/changelog.py | 17 +--- core/checks.py | 4 +- core/clients.py | 25 ++---- core/config.py | 24 ++---- core/models.py | 15 ++-- core/paginator.py | 24 +++--- core/thread.py | 92 ++++++---------------- core/time.py | 5 +- core/utils.py | 5 +- pyproject.toml | 5 +- 18 files changed, 210 insertions(+), 496 deletions(-) diff --git a/.pylintrc b/.pylintrc index a45837fa82..21087a91f7 100644 --- a/.pylintrc +++ b/.pylintrc @@ -267,7 +267,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=99 # Maximum number of lines in a module. max-module-lines=1000 diff --git a/.travis.yml b/.travis.yml index 21035e6f3a..4f7ef0508f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,3 +27,4 @@ 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 + - pipenv run black . --check \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b2959463..f31846610f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ however, insignificant breaking changes does not guarantee a major version bump, - "enable" and "disable" support for yes or no config vars. - Added "perhaps you meant" section to `?config help`. +### Internal + +- Commit to black format line width max = 99, consistent with pylint. + # v3.3.0 diff --git a/bot.py b/bot.py index b9a663f910..48910db578 100644 --- a/bot.py +++ b/bot.py @@ -19,9 +19,10 @@ 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 +from pkg_resources import parse_version + try: # noinspection PyUnresolvedReferences from colorama import init @@ -171,9 +172,7 @@ def run(self, *args, **kwargs): for task in asyncio.all_tasks(self.loop): task.cancel() try: - self.loop.run_until_complete( - asyncio.gather(*asyncio.all_tasks(self.loop)) - ) + self.loop.run_until_complete(asyncio.gather(*asyncio.all_tasks(self.loop))) except asyncio.CancelledError: logger.debug("All pending tasks has been cancelled.") finally: @@ -187,9 +186,7 @@ def owner_ids(self): owner_ids = set(map(int, str(owner_ids).split(","))) if self.owner_id is not None: owner_ids.add(self.owner_id) - permissions = self.config["level_permissions"].get( - PermissionLevel.OWNER.name, [] - ) + permissions = self.config["level_permissions"].get(PermissionLevel.OWNER.name, []) for perm in permissions: owner_ids.add(int(perm)) return owner_ids @@ -216,8 +213,7 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: channel = self.main_category.channels[0] self.config["log_channel_id"] = channel.id logger.warning( - "No log channel set, setting #%s to be the log channel.", - channel.name, + "No log channel set, setting #%s to be the log channel.", channel.name ) return channel except IndexError: @@ -302,9 +298,7 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]: category_id = self.config["main_category_id"] if category_id is not None: try: - cat = discord.utils.get( - self.modmail_guild.categories, id=int(category_id) - ) + cat = discord.utils.get(self.modmail_guild.categories, id=int(category_id)) if cat is not None: return cat except ValueError: @@ -354,9 +348,7 @@ def command_perm(self, command_name: str) -> PermissionLevel: try: return PermissionLevel[level.upper()] except KeyError: - logger.warning( - "Invalid override_command_level for command %s.", command_name - ) + 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) @@ -405,11 +397,7 @@ async def setup_indexes(self): logger.info('Creating "text" index for logs collection.') logger.info("Name: %s", index_name) await coll.create_index( - [ - ("messages.content", "text"), - ("messages.author.name", "text"), - ("key", "text"), - ] + [("messages.content", "text"), ("messages.author.name", "text"), ("key", "text")] ) logger.debug("Successfully configured and verified database indexes.") @@ -428,8 +416,7 @@ async def on_ready(self): 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 + 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) @@ -447,9 +434,7 @@ async def on_ready(self): logger.line() for recipient_id, items in tuple(closures.items()): - after = ( - datetime.fromisoformat(items["time"]) - datetime.utcnow() - ).total_seconds() + after = (datetime.fromisoformat(items["time"]) - datetime.utcnow()).total_seconds() if after < 0: after = 0 @@ -475,9 +460,7 @@ 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"] - ) + logger.debug("Unable to resolve thread with channel %s.", log["channel_id"]) log_data = await self.api.post_log( log["channel_id"], { @@ -494,13 +477,10 @@ 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"], + "Failed to close thread with channel %s, skipping.", log["channel_id"] ) self.metadata_loop = tasks.Loop( @@ -550,9 +530,7 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: return sent_emoji, blocked_emoji - async def _process_blocked( - self, message: discord.Message - ) -> typing.Tuple[bool, str]: + 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: @@ -597,9 +575,7 @@ async def _process_blocked( logger.debug("Blocked due to account age, user %s.", message.author.name) if str(message.author.id) not in self.blocked_users: - new_reason = ( - f"System Message: New Account. Required to wait for {delta}." - ) + new_reason = f"System Message: New Account. Required to wait for {delta}." self.blocked_users[str(message.author.id)] = new_reason changed = True @@ -607,8 +583,7 @@ async def _process_blocked( await message.channel.send( embed=discord.Embed( title="Message not sent!", - description=f"Your must wait for {delta} " - f"before you can contact me.", + description=f"Your must wait for {delta} before you can contact me.", color=self.error_color, ) ) @@ -621,9 +596,7 @@ async def _process_blocked( logger.debug("Blocked due to guild age, user %s.", message.author.name) if str(message.author.id) not in self.blocked_users: - new_reason = ( - f"System Message: Recently Joined. Required to wait for {delta}." - ) + new_reason = f"System Message: Recently Joined. Required to wait for {delta}." self.blocked_users[str(message.author.id)] = new_reason changed = True @@ -631,8 +604,7 @@ async def _process_blocked( await message.channel.send( embed=discord.Embed( title="Message not sent!", - description=f"Your must wait for {delta} " - f"before you can contact me.", + description=f"Your must wait for {delta} before you can contact me.", color=self.error_color, ) ) @@ -643,9 +615,7 @@ async def _process_blocked( ): # Met the age limit already, otherwise it would've been caught by the previous if's reaction = sent_emoji - logger.debug( - "No longer internally blocked, user %s.", message.author.name - ) + logger.debug("No longer internally blocked, user %s.", message.author.name) self.blocked_users.pop(str(message.author.id)) else: reaction = blocked_emoji @@ -661,9 +631,7 @@ async def _process_blocked( ) if end_time is not None: - after = ( - datetime.fromisoformat(end_time.group(1)) - now - ).total_seconds() + after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() if after <= 0: # No longer blocked reaction = sent_emoji @@ -701,12 +669,10 @@ async def process_dm_modmail(self, message: discord.Message) -> None: description=self.config["disabled_new_thread_response"], ) embed.set_footer( - text=self.config["disabled_new_thread_footer"], - icon_url=self.guild.icon_url, + 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, + "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) @@ -724,8 +690,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: icon_url=self.guild.icon_url, ) logger.info( - "A message was blocked from %s due to disabled Modmail.", - message.author, + "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) @@ -866,9 +831,7 @@ async def process_commands(self, message): for ctx in ctxs: if ctx.command: if not any( - 1 - for check in ctx.command.checks - if hasattr(check, "permission_level") + 1 for check in ctx.command.checks if hasattr(check, "permission_level") ): logger.debug( "Command %s has no permissions check, adding invalid level.", @@ -1064,9 +1027,7 @@ async def on_command_error(self, context, exception): [c.__name__ for c in exception.converters] ) await context.trigger_typing() - await context.send( - embed=discord.Embed(color=self.error_color, description=msg) - ) + await context.send(embed=discord.Embed(color=self.error_color, description=msg)) elif isinstance(exception, commands.BadArgument): await context.trigger_typing() @@ -1082,9 +1043,7 @@ async def on_command_error(self, context, exception): if not await check(context): if hasattr(check, "fail_msg"): await context.send( - embed=discord.Embed( - color=self.error_color, description=check.fail_msg - ) + embed=discord.Embed(color=self.error_color, description=check.fail_msg) ) if hasattr(check, "permission_level"): corrected_permission_level = self.command_perm( diff --git a/cogs/modmail.py b/cogs/modmail.py index 8ce3f838fe..6a2b3bbfe8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -61,9 +61,7 @@ async def setup(self, ctx): return await ctx.send(embed=embed) overwrites = { - self.bot.modmail_guild.default_role: discord.PermissionOverwrite( - read_messages=False - ), + self.bot.modmail_guild.default_role: discord.PermissionOverwrite(read_messages=False), self.bot.modmail_guild.me: discord.PermissionOverwrite(read_messages=True), } @@ -108,9 +106,7 @@ async def setup(self, ctx): "feeling generous, check us out on [Patreon](https://patreon.com/kyber)!", ) - embed.set_footer( - text=f'Type "{self.bot.prefix}help" for a complete list of commands.' - ) + embed.set_footer(text=f'Type "{self.bot.prefix}help" for a complete list of commands.') await log_channel.send(embed=embed) self.bot.config["main_category_id"] = category.id @@ -126,10 +122,7 @@ async def setup(self, ctx): f"- `{self.bot.prefix}config help` for a list of available customizations." ) - if ( - not self.bot.config["command_permissions"] - and not self.bot.config["level_permissions"] - ): + if not self.bot.config["command_permissions"] and not self.bot.config["level_permissions"]: await self.bot.update_perms(PermissionLevel.REGULAR, -1) for owner_ids in self.bot.owner_ids: await self.bot.update_perms(PermissionLevel.OWNER, owner_ids) @@ -161,28 +154,21 @@ async def snippet(self, ctx, *, name: str.lower = None): if name is not None: val = self.bot.snippets.get(name) if val is None: - embed = create_not_found_embed( - name, self.bot.snippets.keys(), "Snippet" - ) + embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") return await ctx.send(embed=embed) return await ctx.send(escape_mentions(val)) if not self.bot.snippets: embed = discord.Embed( - color=self.bot.error_color, - description="You dont have any snippets at the moment.", - ) - embed.set_footer( - text=f"Do {self.bot.prefix}help snippet for more commands." + color=self.bot.error_color, description="You dont have any snippets at the moment." ) + embed.set_footer(text=f"Do {self.bot.prefix}help snippet for more commands.") embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) return await ctx.send(embed=embed) embeds = [] - for i, names in enumerate( - zip_longest(*(iter(sorted(self.bot.snippets)),) * 15) - ): + for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)): description = format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) @@ -233,7 +219,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte embed = discord.Embed( title="Error", color=self.bot.error_color, - description=f"Snippet names cannot be longer than 120 characters.", + description="Snippet names cannot be longer than 120 characters.", ) return await ctx.send(embed=embed) @@ -243,7 +229,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte embed = discord.Embed( title="Added snippet", color=self.bot.main_color, - description=f"Successfully created snippet.", + description="Successfully created snippet.", ) return await ctx.send(embed=embed) @@ -290,9 +276,7 @@ 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. @@ -336,9 +320,7 @@ async def send_scheduled_close_message(self, ctx, after, silent=False): if after.arg and not silent: embed.add_field(name="Message", value=after.arg) - embed.set_footer( - text="Closing will be cancelled " "if a thread message is sent." - ) + embed.set_footer(text="Closing will be cancelled if a thread message is sent.") embed.timestamp = after.dt await ctx.send(embed=embed) @@ -380,8 +362,7 @@ 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=self.bot.error_color, - description="Scheduled close has been cancelled.", + color=self.bot.error_color, description="Scheduled close has been cancelled." ) else: embed = discord.Embed( @@ -394,9 +375,7 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): if after and after.dt > now: await self.send_scheduled_close_message(ctx, after, silent) - await thread.close( - closer=ctx.author, after=close_after, message=message, silent=silent - ) + await thread.close(closer=ctx.author, after=close_after, message=message, silent=silent) @staticmethod def parse_user_or_role(ctx, user_or_role): @@ -445,8 +424,7 @@ async def notify( await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=f"{mention} will be mentioned " - "on the next message received.", + description=f"{mention} will be mentioned on the next message received.", ) return await ctx.send(embed=embed) @@ -483,8 +461,7 @@ async def unnotify( mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( - color=self.bot.main_color, - description=f"{mention} will no longer be notified.", + color=self.bot.main_color, description=f"{mention} will no longer be notified." ) return await ctx.send(embed=embed) @@ -517,15 +494,14 @@ async def subscribe( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=f"{mention} is already " "subscribed to this thread.", + description=f"{mention} is already subscribed to this thread.", ) else: mentions.append(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=f"{mention} will now be " - "notified of all messages received.", + description=f"{mention} will now be notified of all messages received.", ) return await ctx.send(embed=embed) @@ -556,14 +532,14 @@ async def unsubscribe( if mention not in mentions: embed = discord.Embed( color=self.bot.error_color, - description=f"{mention} is not already " "subscribed to this thread.", + description=f"{mention} is not already subscribed to this thread.", ) else: mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=f"{mention} is now unsubscribed " "to this thread.", + description=f"{mention} is now unsubscribed to this thread.", ) return await ctx.send(embed=embed) @@ -597,9 +573,7 @@ async def sfw(self, ctx): async def loglink(self, ctx): """Retrieves the link to the current thread's logs.""" log_link = await self.bot.api.get_log_link(ctx.channel.id) - await ctx.send( - embed=discord.Embed(color=self.bot.main_color, description=log_link) - ) + await ctx.send(embed=discord.Embed(color=self.bot.main_color, description=log_link)) def format_log_embeds(self, logs, avatar_url): embeds = [] @@ -618,13 +592,9 @@ def format_log_embeds(self, logs, avatar_url): username += entry["recipient"]["discriminator"] embed = discord.Embed(color=self.bot.main_color, timestamp=created_at) - embed.set_author( - name=f"{title} - {username}", icon_url=avatar_url, url=log_url - ) + embed.set_author(name=f"{title} - {username}", icon_url=avatar_url, url=log_url) embed.url = log_url - embed.add_field( - name="Created", value=duration(created_at, now=datetime.utcnow()) - ) + embed.add_field(name="Created", value=duration(created_at, now=datetime.utcnow())) closer = entry.get("closer") if closer is None: closer_msg = "Unknown" @@ -635,9 +605,7 @@ def format_log_embeds(self, logs, avatar_url): if entry["recipient"]["id"] != entry["creator"]["id"]: embed.add_field(name="Created by", value=f"<@{entry['creator']['id']}>") - embed.add_field( - name="Preview", value=format_preview(entry["messages"]), inline=False - ) + embed.add_field(name="Preview", value=format_preview(entry["messages"]), inline=False) if closer is not None: # BUG: Currently, logviewer can't display logs without a closer. @@ -677,7 +645,7 @@ async def logs(self, ctx, *, user: User = None): if not any(not log["open"] for log in logs): embed = discord.Embed( color=self.bot.error_color, - description="This user does not " "have any previous logs.", + description="This user does not have any previous logs.", ) return await ctx.send(embed=embed) @@ -699,11 +667,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): """ user = user if user is not None else ctx.author - query = { - "guild_id": str(self.bot.guild_id), - "open": False, - "closer.id": str(user.id), - } + query = {"guild_id": str(self.bot.guild_id), "open": False, "closer.id": str(user.id)} projection = {"messages": {"$slice": 5}} @@ -922,8 +886,7 @@ async def contact( if user.bot: embed = discord.Embed( - color=self.bot.error_color, - description="Cannot start a thread with a bot.", + color=self.bot.error_color, description="Cannot start a thread with a bot." ) return await ctx.send(embed=embed) @@ -937,16 +900,13 @@ async def contact( await ctx.channel.send(embed=embed) else: - thread = self.bot.threads.create( - user, creator=ctx.author, category=category - ) + 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} " - f"for {user.mention}.", + description=f"Thread started by {ctx.author.mention} for {user.mention}.", color=self.bot.main_color, ) await thread.wait_until_ready() @@ -965,11 +925,7 @@ async def contact( async def blocked(self, ctx): """Retrieve a list of blocked users.""" - embeds = [ - discord.Embed( - title="Blocked Users", color=self.bot.main_color, description="" - ) - ] + embeds = [discord.Embed(title="Blocked Users", color=self.bot.main_color, description="")] users = [] @@ -1062,9 +1018,7 @@ async def blocked_whitelist(self, ctx, *, user: User = None): @commands.command(usage="[user] [duration] [close message]") @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing - async def block( - self, ctx, user: Optional[User] = None, *, after: UserFriendlyTime = None - ): + async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTime = None): """ Block a user from using Modmail. @@ -1179,9 +1133,7 @@ async def unblock(self, ctx, *, user: User = None): ) else: embed = discord.Embed( - title="Error", - description=f"{mention} is not blocked.", - color=self.bot.error_color, + title="Error", description=f"{mention} is not blocked.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -1204,9 +1156,7 @@ async def delete(self, ctx, message_id: Optional[int] = None): try: message_id = int(message_id) except ValueError: - raise commands.BadArgument( - "An integer message ID needs to be specified." - ) + raise commands.BadArgument("An integer message ID needs to be specified.") linked_message_id = await self.find_linked_message(ctx, message_id) @@ -1236,7 +1186,7 @@ async def enable(self, ctx): """ embed = discord.Embed( title="Success", - description=f"Modmail will now accept all DM messages.", + description="Modmail will now accept all DM messages.", color=self.bot.main_color, ) @@ -1257,7 +1207,7 @@ async def disable(self, ctx): """ embed = discord.Embed( title="Success", - description=f"Modmail will not create any new threads.", + description="Modmail will not create any new threads.", color=self.bot.main_color, ) if self.bot.config["dm_disabled"] < 1: @@ -1276,7 +1226,7 @@ async def disable_all(self, ctx): """ embed = discord.Embed( title="Success", - description=f"Modmail will not accept any DM messages.", + description="Modmail will not accept any DM messages.", color=self.bot.main_color, ) @@ -1296,19 +1246,19 @@ async def isenable(self, ctx): if self.bot.config["dm_disabled"] == 1: embed = discord.Embed( title="New Threads Disabled", - description=f"Modmail is not creating new threads.", + description="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 is not accepting any DM messages for new and existing threads.", + description="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 accepting all DM messages.", + description="Modmail is accepting all DM messages.", color=self.bot.main_color, ) diff --git a/cogs/plugins.py b/cogs/plugins.py index 15db8214e4..8060799ce2 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -41,9 +41,7 @@ def __init__(self, user, repo, name, branch=None): @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): @@ -76,8 +74,7 @@ def from_string(cls, s, strict=False): 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) # pylint: disable=raising-format-tuple def __hash__(self): return hash((self.user, self.repo, self.name, self.branch)) @@ -129,14 +126,10 @@ 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: @@ -212,9 +205,7 @@ async def load_plugin(self, plugin): if stderr: logger.debug("[stderr]\n%s.", stderr.decode()) logger.error( - "Failed to download requirements for %s.", - plugin.ext_string, - exc_info=True, + "Failed to download requirements for %s.", plugin.ext_string, exc_info=True ) raise InvalidPluginError( f"Unable to download requirements: ```\n{stderr.decode()}\n```" @@ -250,9 +241,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): 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}`.", @@ -293,7 +282,8 @@ async def plugins_add(self, ctx, *, plugin_name: str): """ Install a new 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_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) @@ -302,8 +292,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=self.bot.error_color, + description="This plugin is already installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -324,10 +313,10 @@ async def plugins_add(self, ctx, *, plugin_name: str): try: await self.download_plugin(plugin, force=True) except Exception: - logger.warning(f"Unable to download plugin %s.", plugin, exc_info=True) + logger.warning("Unable to download plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", + description="Failed to download plugin, check logs for error.", color=self.bot.error_color, ) @@ -343,10 +332,10 @@ async def plugins_add(self, ctx, *, plugin_name: str): try: await self.load_plugin(plugin) except Exception: - logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) + logger.warning("Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", + description="Failed to download plugin, check logs for error.", color=self.bot.error_color, ) @@ -409,8 +398,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): pass # dir not empty embed = discord.Embed( - description="The plugin is successfully uninstalled.", - color=self.bot.main_color, + description="The plugin is successfully uninstalled.", color=self.bot.main_color ) await ctx.send(embed=embed) @@ -436,8 +424,7 @@ async def update_plugin(self, ctx, plugin_name): 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, + description=f"Successfully updated {plugin.name}.", color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -454,6 +441,7 @@ async def plugins_update(self, ctx, *, plugin_name: str = None): """ if plugin_name is None: + # pylint: disable=redefined-argument-from-local for plugin_name in self.bot.config["plugins"]: await self.update_plugin(ctx, plugin_name) else: @@ -483,8 +471,7 @@ async def plugins_loaded(self, ctx): if not self.loaded_plugins: embed = discord.Embed( - description="There are no plugins currently loaded.", - color=self.bot.error_color, + description="There are no plugins currently loaded.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -510,13 +497,9 @@ async def plugins_loaded(self, ctx): paginator = EmbedPaginatorSession(ctx, *embeds) await paginator.run() - @plugins.group( - invoke_without_command=True, name="registry", aliases=["list", "info"] - ) + @plugins.group(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. @@ -539,9 +522,7 @@ async def plugins_registry( if index >= len(registry): index = len(registry) - 1 else: - index = next( - (i for i, (n, _) in enumerate(registry) if plugin_name == n), 0 - ) + index = next((i for i, (n, _) in enumerate(registry) if plugin_name == n), 0) if not index and plugin_name is not None: embed = discord.Embed( @@ -553,8 +534,7 @@ async def plugins_registry( if matches: embed.add_field( - name="Perhaps you meant:", - value="\n".join(f"`{m}`" for m in matches), + name="Perhaps you meant:", value="\n".join(f"`{m}`" for m in matches) ) return await ctx.send(embed=embed) @@ -574,8 +554,7 @@ async def plugins_registry( ) 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( @@ -592,11 +571,9 @@ async def plugins_registry( 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, " + text="Your bot is unable to install this plugin, " f"minimum required version is v{required_version}." ) else: @@ -628,9 +605,7 @@ async def plugins_registry_compact(self, ctx): 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}" diff --git a/cogs/utility.py b/cogs/utility.py index b540483174..392656b650 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -67,11 +67,7 @@ async def format_cog_help(self, cog, *, no_cog=False): embed.add_field(name="Commands", value=format_ or "No commands.") continued = " (Continued)" if embeds else "" - name = ( - cog.qualified_name + " - Help" - if not no_cog - else "Miscellaneous Commands" - ) + name = cog.qualified_name + " - Help" if not no_cog else "Miscellaneous Commands" embed.set_author(name=name + continued, icon_url=bot.user.avatar_url) embed.set_footer( @@ -92,11 +88,7 @@ async def send_bot_help(self, mapping): bot = self.context.bot # always come first - default_cogs = [ - bot.get_cog("Modmail"), - bot.get_cog("Utility"), - bot.get_cog("Plugins"), - ] + default_cogs = [bot.get_cog("Modmail"), bot.get_cog("Utility"), bot.get_cog("Plugins")] default_cogs.extend(c for c in cogs if c not in default_cogs) @@ -105,16 +97,12 @@ async def send_bot_help(self, mapping): if no_cog_commands: embeds.extend(await self.format_cog_help(no_cog_commands, no_cog=True)) - session = EmbedPaginatorSession( - self.context, *embeds, destination=self.get_destination() - ) + session = EmbedPaginatorSession(self.context, *embeds, destination=self.get_destination()) return await session.run() async def send_cog_help(self, cog): embeds = await self.format_cog_help(cog) - session = EmbedPaginatorSession( - self.context, *embeds, destination=self.get_destination() - ) + session = EmbedPaginatorSession(self.context, *embeds, destination=self.get_destination()) return await session.run() async def _get_help_embed(self, topic): @@ -173,7 +161,7 @@ async def send_error_message(self, error): val = self.context.bot.snippets.get(command) if val is not None: return await self.get_destination().send( - escape_mentions(f"**`{command}` is a snippet, " f"content:**\n\n{val}") + escape_mentions(f"**`{command}` is a snippet, content:**\n\n{val}") ) val = self.context.bot.aliases.get(command) @@ -213,9 +201,7 @@ async def send_error_message(self, error): closest = get_close_matches(command, choices) if closest: - embed.add_field( - name=f"Perhaps you meant:", value="\n".join(f"`{x}`" for x in closest) - ) + embed.add_field(name="Perhaps you meant:", value="\n".join(f"`{x}`" for x in closest)) else: embed.title = "Cannot find command or category" embed.set_footer( @@ -239,7 +225,7 @@ def __init__(self, bot): }, ) self.bot.help_command.cog = self - self.loop_presence.start() + self.loop_presence.start() # pylint: disable=no-member def cog_unload(self): self.bot.help_command = self._original_help_command @@ -305,12 +291,11 @@ async def about(self, ctx): if self.bot.version.is_prerelease: stable = next( - filter( - lambda v: not parse_version(v.version).is_prerelease, - changelog.versions, - ) + 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}." ) - 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: @@ -365,8 +350,7 @@ async def debug(self, ctx): with open( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - f"../temp/{log_file_name}.log", + os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "r+", ) as f: @@ -421,17 +405,14 @@ async def debug_hastebin(self, ctx): with open( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - f"../temp/{log_file_name}.log", + os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "rb+", ) as f: logs = BytesIO(f.read().strip()) try: - async with self.bot.session.post( - haste_url + "/documents", data=logs - ) as resp: + async with self.bot.session.post(haste_url + "/documents", data=logs) as resp: data = await resp.json() try: key = data["key"] @@ -447,8 +428,7 @@ async def debug_hastebin(self, ctx): embed = discord.Embed( title="Debug Logs", color=self.bot.main_color, - description="Something's wrong. " - "We're unable to upload your logs to hastebin.", + description="Something's wrong. We're unable to upload your logs to hastebin.", ) embed.set_footer(text="Go to Heroku to see your logs.") await ctx.send(embed=embed) @@ -463,8 +443,7 @@ async def debug_clear(self, ctx): with open( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - f"../temp/{log_file_name}.log", + os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "w", ): @@ -527,9 +506,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): else: msg += f"{activity.name}." - embed = discord.Embed( - title="Activity Changed", description=msg, color=self.bot.main_color - ) + embed = discord.Embed(title="Activity Changed", description=msg, color=self.bot.main_color) return await ctx.send(embed=embed) @commands.command() @@ -566,14 +543,10 @@ async def status(self, ctx, *, status_type: str.lower): 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 - ) + 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=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") @@ -582,9 +555,7 @@ async def set_presence( 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".' @@ -600,9 +571,7 @@ async def set_presence( url = self.bot.config["twitch_url"] if activity_type is not None: - activity = discord.Activity( - type=activity_type, name=activity_message, url=url - ) + activity = discord.Activity(type=activity_type, name=activity_message, url=url) else: activity = None await self.bot.change_presence(activity=activity, status=status) @@ -664,9 +633,7 @@ async def mention(self, ctx, *, mention: str = None): if mention is None: embed = discord.Embed( - title="Current mention:", - color=self.bot.main_color, - description=str(current), + title="Current mention:", color=self.bot.main_color, description=str(current) ) else: embed = discord.Embed( @@ -761,9 +728,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): embed = exc.embed else: embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"{key} is an invalid key.", + title="Error", color=self.bot.error_color, description=f"{key} is an invalid key." ) valid_keys = [f"`{k}`" for k in sorted(keys)] embed.add_field(name="Valid keys", value=", ".join(valid_keys)) @@ -785,9 +750,7 @@ async def config_remove(self, ctx, *, key: str.lower): ) else: embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"{key} is an invalid key.", + title="Error", color=self.bot.error_color, description=f"{key} is an invalid key." ) valid_keys = [f"`{k}`" for k in sorted(keys)] embed.add_field(name="Valid keys", value=", ".join(valid_keys)) @@ -808,9 +771,7 @@ async def config_get(self, ctx, *, key: str.lower = None): if key in keys: desc = f"`{key}` is set to `{self.bot.config[key]}`" embed = discord.Embed(color=self.bot.main_color, description=desc) - embed.set_author( - name="Config variable", icon_url=self.bot.user.avatar_url - ) + embed.set_author(name="Config variable", icon_url=self.bot.user.avatar_url) else: embed = discord.Embed( @@ -825,12 +786,9 @@ async def config_get(self, ctx, *, key: str.lower = None): else: embed = discord.Embed( color=self.bot.main_color, - description="Here is a list of currently " - "set configuration variable(s).", - ) - embed.set_author( - name="Current config(s):", icon_url=self.bot.user.avatar_url + description="Here is a list of currently set configuration variable(s).", ) + embed.set_author(name="Current config(s):", icon_url=self.bot.user.avatar_url) config = self.bot.config.filter_default(self.bot.config) for name, value in config.items(): @@ -858,8 +816,7 @@ async def config_help(self, ctx, key: str.lower = None): ) if closest: embed.add_field( - name=f"Perhaps you meant:", - value="\n".join(f"`{x}`" for x in closest), + name=f"Perhaps you meant:", value="\n".join(f"`{x}`" for x in closest) ) return await ctx.send(embed=embed) @@ -882,13 +839,10 @@ def fmt(val): if current_key == key: index = i embed = discord.Embed( - title=f"Configuration description on {current_key}:", - color=self.bot.main_color, + title=f"Configuration description on {current_key}:", color=self.bot.main_color ) embed.add_field(name="Default:", value=fmt(info["default"]), inline=False) - embed.add_field( - name="Information:", value=fmt(info["description"]), inline=False - ) + embed.add_field(name="Information:", value=fmt(info["description"]), inline=False) if info["examples"]: example_text = "" for example in info["examples"]: @@ -937,9 +891,7 @@ async def alias(self, ctx, *, name: str.lower = None): if name is not None: val = self.bot.aliases.get(name) if val is None: - embed = utils.create_not_found_embed( - name, self.bot.aliases.keys(), "Alias" - ) + embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias") return await ctx.send(embed=embed) values = utils.parse_alias(val) @@ -949,7 +901,7 @@ async def alias(self, ctx, *, name: str.lower = None): title="Error", 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.", + "This alias will now be deleted.", ) self.bot.aliases.pop(name) await self.bot.config.update() @@ -972,8 +924,7 @@ async def alias(self, ctx, *, name: str.lower = None): if not self.bot.aliases: embed = discord.Embed( - color=self.bot.error_color, - description="You dont have any aliases at the moment.", + 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.") embed.set_author(name="Aliases", icon_url=ctx.guild.icon_url) @@ -1044,7 +995,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed = discord.Embed( title="Error", color=self.bot.error_color, - description=f"Alias names cannot be longer than 120 characters.", + description="Alias names cannot be longer than 120 characters.", ) if embed is not None: @@ -1264,9 +1215,7 @@ def _parse_level(name): @permissions.command(name="override") @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_override( - self, ctx, command_name: str.lower, *, level_name: str - ): + async def permissions_override(self, ctx, command_name: str.lower, *, level_name: str): """ Change a permission level for a specific command. @@ -1305,9 +1254,7 @@ async def permissions_override( command.qualified_name, level.name, ) - self.bot.config["override_command_level"][ - command.qualified_name - ] = level.name + self.bot.config["override_command_level"][command.qualified_name] = level.name await self.bot.config.update() embed = discord.Embed( @@ -1379,9 +1326,7 @@ async def permissions_add( key = self.bot.modmail_guild.get_member(value) if key is not None: logger.info("Granting %s access to Modmail category.", key.name) - await self.bot.main_category.set_permissions( - key, read_messages=True - ) + await self.bot.main_category.set_permissions(key, read_messages=True) embed = discord.Embed( title="Success", @@ -1478,21 +1423,13 @@ async def permissions_remove( self.bot.modmail_guild.default_role, read_messages=False ) elif isinstance(user_or_role, discord.Role): - logger.info( - "Denying %s access to Modmail category.", user_or_role.name - ) - await self.bot.main_category.set_permissions( - user_or_role, overwrite=None - ) + logger.info("Denying %s access to Modmail category.", user_or_role.name) + await self.bot.main_category.set_permissions(user_or_role, overwrite=None) else: member = self.bot.modmail_guild.get_member(value) if member is not None and member != self.bot.modmail_guild.me: - logger.info( - "Denying %s access to Modmail category.", member.name - ) - await self.bot.main_category.set_permissions( - member, overwrite=None - ) + logger.info("Denying %s access to Modmail category.", member.name) + await self.bot.main_category.set_permissions(member, overwrite=None) embed = discord.Embed( title="Success", @@ -1542,11 +1479,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[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. @@ -1596,9 +1529,7 @@ async def permissions_get( if value in permissions: levels.append(level.name) - mention = getattr( - user_or_role, "name", getattr(user_or_role, "id", user_or_role) - ) + mention = getattr(user_or_role, "name", getattr(user_or_role, "id", user_or_role)) desc_cmd = ( ", ".join(map(lambda x: f"`{x}`", cmds)) if cmds @@ -1648,14 +1579,10 @@ async def permissions_get( ) ) 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 = discord.Embed( color=self.bot.main_color, description=description @@ -1711,9 +1638,7 @@ async def permissions_get( return await ctx.send(embed=embed) if user_or_role == "command": - embeds.append( - self._get_perm(ctx, command.qualified_name, "command") - ) + embeds.append(self._get_perm(ctx, command.qualified_name, "command")) else: embeds.append(self._get_perm(ctx, level.name, "level")) else: @@ -1722,9 +1647,7 @@ async def permissions_get( for command in self.bot.walk_commands(): if command not in done: done.add(command) - embeds.append( - self._get_perm(ctx, command.qualified_name, "command") - ) + embeds.append(self._get_perm(ctx, command.qualified_name, "command")) else: for perm_level in PermissionLevel: embeds.append(self._get_perm(ctx, perm_level.name, "level")) @@ -1766,12 +1689,10 @@ async def oauth_whitelist(self, ctx, target: Union[discord.Role, utils.User]): embed.title = "Success" if not hasattr(target, "mention"): - target = self.bot.get_user(target.id) or self.bot.modmail_guild.get_role( - target.id - ) + target = self.bot.get_user(target.id) or self.bot.modmail_guild.get_role(target.id) embed.description = ( - f"{'Un-w' if removed else 'W'}hitelisted " f"{target.mention} to view logs." + f"{'Un-w' if removed else 'W'}hitelisted {target.mention} to view logs." ) await ctx.send(embed=embed) @@ -1796,12 +1717,8 @@ async def oauth_show(self, ctx): embed = discord.Embed(color=self.bot.main_color) embed.title = "Oauth Whitelist" - embed.add_field( - name="Users", value=" ".join(u.mention for u in users) or "None" - ) - embed.add_field( - name="Roles", value=" ".join(r.mention for r in roles) or "None" - ) + embed.add_field(name="Users", value=" ".join(u.mention for u in users) or "None") + embed.add_field(name="Roles", value=" ".join(r.mention for r in roles) or "None") await ctx.send(embed=embed) diff --git a/core/_color_data.py b/core/_color_data.py index ad98d3856f..0ac42d5c1f 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -43,9 +43,7 @@ } # 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 91856600e9..ace825482f 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -51,9 +51,7 @@ 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() @@ -91,9 +89,7 @@ def embed(self) -> Embed: """ 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, - url=self.url, + name=f"v{self.version} - Changelog", icon_url=self.bot.user.avatar_url, url=self.url ) for name, value in self.fields.items(): @@ -138,9 +134,7 @@ 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: @@ -174,10 +168,7 @@ async def from_url(cls, bot, url: str = "") -> "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" - ) + 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/checks.py b/core/checks.py index 0a5a43e1bb..7d387e6a5b 100644 --- a/core/checks.py +++ b/core/checks.py @@ -5,9 +5,7 @@ logger = getLogger(__name__) -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) diff --git a/core/clients.py b/core/clients.py index 8d89331664..7bc17943e5 100644 --- a/core/clients.py +++ b/core/clients.py @@ -66,9 +66,7 @@ async def request( `str` if the returned data is not a valid json data, the raw response. """ - async with self.session.request( - method, url, headers=headers, json=payload - ) as resp: + async with self.session.request(method, url, headers=headers, json=payload) as resp: if return_response: return resp try: @@ -120,7 +118,9 @@ async def get_log_link(self, channel_id: Union[str, int]) -> str: prefix = self.bot.config["log_url_prefix"].strip("/") if prefix == "NONE": prefix = "" - return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{doc['key']}" + return ( + f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{doc['key']}" + ) async def create_log_entry( self, recipient: Member, channel: TextChannel, creator: Member @@ -184,13 +184,9 @@ async def update_config(self, data: dict): {"bot_id": self.bot.user.id}, {"$set": toset, "$unset": unset} ) if toset: - return await self.db.config.update_one( - {"bot_id": self.bot.user.id}, {"$set": toset} - ) + return await self.db.config.update_one({"bot_id": self.bot.user.id}, {"$set": toset}) if unset: - return await self.db.config.update_one( - {"bot_id": self.bot.user.id}, {"$unset": unset} - ) + return await self.db.config.update_one({"bot_id": self.bot.user.id}, {"$unset": unset}) async def edit_message(self, message_id: Union[int, str], new_content: str) -> None: await self.logs.update_one( @@ -199,10 +195,7 @@ async def edit_message(self, message_id: Union[int, str], new_content: str) -> N ) async def append_log( - self, - message: Message, - channel_id: Union[str, int] = "", - type_: str = "thread_message", + self, message: Message, channel_id: Union[str, int] = "", type_: str = "thread_message" ) -> dict: channel_id = str(channel_id) or str(message.channel.id) data = { @@ -230,9 +223,7 @@ async def append_log( } return await self.logs.find_one_and_update( - {"channel_id": channel_id}, - {"$push": {f"messages": data}}, - return_document=True, + {"channel_id": channel_id}, {"$push": {"messages": data}}, return_document=True ) async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: diff --git a/core/config.py b/core/config.py index bfc4a3b675..ec8e66936d 100644 --- a/core/config.py +++ b/core/config.py @@ -146,9 +146,7 @@ def populate_cache(self) -> dict: data = deepcopy(self.defaults) # populate from env var and .env file - data.update( - {k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys} - ) + data.update({k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys}) config_json = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json" ) @@ -165,9 +163,7 @@ def populate_cache(self) -> dict: } ) except json.JSONDecodeError: - logger.critical( - "Failed to load config.json env values.", exc_info=True - ) + logger.critical("Failed to load config.json env values.", exc_info=True) self._cache = data config_help_json = os.path.join( @@ -229,7 +225,7 @@ def get(self, key: str, convert=True) -> typing.Any: elif key in self.time_deltas: if value is None: - return + return None try: value = isodate.parse_duration(value) except isodate.ISO8601Error: @@ -248,7 +244,7 @@ def get(self, key: str, convert=True) -> typing.Any: elif key in self.special_types: if value is None: - return + return None if key == "status": try: @@ -303,9 +299,7 @@ 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: @@ -343,9 +337,7 @@ def items(self) -> typing.Iterable: return self._cache.items() @classmethod - def filter_valid( - cls, data: typing.Dict[str, typing.Any] - ) -> typing.Dict[str, typing.Any]: + def filter_valid(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: return { k.lower(): v for k, v in data.items() @@ -353,9 +345,7 @@ def filter_valid( } @classmethod - def filter_default( - cls, data: typing.Dict[str, typing.Any] - ) -> typing.Dict[str, typing.Any]: + def filter_default(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: # TODO: use .get to prevent errors filtered = {} for k, v in data.items(): diff --git a/core/models.py b/core/models.py index 5a1b455f61..f55526f573 100644 --- a/core/models.py +++ b/core/models.py @@ -1,4 +1,3 @@ -import _string import logging import re import sys @@ -8,6 +7,8 @@ import discord from discord.ext import commands +import _string + try: from colorama import Fore, Style except ImportError: @@ -34,9 +35,7 @@ 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): @@ -82,10 +81,7 @@ def line(self, level="info"): if self.isEnabledFor(level): self._log( level, - Fore.BLACK - + Style.BRIGHT - + "-------------------------" - + Style.RESET_ALL, + Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL, [], ) @@ -97,8 +93,7 @@ def line(self, level="info"): 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", + "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", datefmt="%m/%d/%y %H:%M:%S" ) ch.setFormatter(formatter) diff --git a/core/paginator.py b/core/paginator.py index 835337dbed..695d194415 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -214,30 +214,28 @@ def __init__(self, ctx: commands.Context, *embeds, **options): footer_text = footer_text + " • " + embed.footer.text embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url) - def add_page(self, embed: Embed) -> None: - if isinstance(embed, Embed): - self.pages.append(embed) + def add_page(self, item: Embed) -> None: + if isinstance(item, Embed): + self.pages.append(item) else: raise TypeError("Page must be an Embed object.") - async def _create_base(self, embed: Embed) -> None: - self.base = await self.destination.send(embed=embed) + async def _create_base(self, item: Embed) -> None: + self.base = await self.destination.send(embed=item) async def _show_page(self, page): await self.base.edit(embed=page) class MessagePaginatorSession(PaginatorSession): - def __init__( - self, ctx: commands.Context, *messages, embed: Embed = None, **options - ): + def __init__(self, ctx: commands.Context, *messages, embed: Embed = None, **options): self.embed = embed self.footer_text = self.embed.footer.text if embed is not None else None super().__init__(ctx, *messages, **options) - def add_page(self, msg: str) -> None: - if isinstance(msg, str): - self.pages.append(msg) + def add_page(self, item: str) -> None: + if isinstance(item, str): + self.pages.append(item) else: raise TypeError("Page must be a str object.") @@ -248,9 +246,9 @@ def _set_footer(self): footer_text = footer_text + " • " + self.footer_text self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url) - async def _create_base(self, msg: str) -> None: + async def _create_base(self, item: str) -> None: self._set_footer() - self.base = await self.ctx.send(content=msg, embed=self.embed) + self.base = await self.ctx.send(content=item, embed=self.embed) async def _show_page(self, page) -> None: self._set_footer() diff --git a/core/thread.py b/core/thread.py index 03cefcae3d..9a40c07fd8 100644 --- a/core/thread.py +++ b/core/thread.py @@ -43,10 +43,7 @@ def __init__( self.auto_close_task = None def __repr__(self): - return ( - f'Thread(recipient="{self.recipient or self.id}", ' - f"channel={self.channel.id})" - ) + return f'Thread(recipient="{self.recipient or self.id}", ' f"channel={self.channel.id})" async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" @@ -85,9 +82,7 @@ async def setup(self, *, creator=None, category=None): # in case it creates a channel outside of category overwrites = { - self.bot.modmail_guild.default_role: discord.PermissionOverwrite( - read_messages=False - ) + self.bot.modmail_guild.default_role: discord.PermissionOverwrite(read_messages=False) } category = category or self.bot.main_category @@ -125,9 +120,7 @@ async def setup(self, *, creator=None, category=None): log_count = sum(1 for log in log_data if not log["open"]) except Exception: - logger.error( - "An error occurred while posting logs to the database.", exc_info=True - ) + logger.error("An error occurred while posting logs to the database.", exc_info=True) log_url = log_count = None # ensure core functionality still works @@ -211,9 +204,7 @@ def _format_info_embed(self, user, log_url, log_count, color): created = str((time - user.created_at).days) embed = discord.Embed( - color=color, - description=f"{user.mention} was created {days(created)}", - timestamp=time, + color=color, description=f"{user.mention} was created {days(created)}", timestamp=time ) # if not role_names: @@ -238,7 +229,7 @@ def _format_info_embed(self, user, log_url, log_count, color): embed.set_footer(text=f"{footer} • (not in main server)") if log_count is not None: - # embed.add_field(name='Past logs', value=f'{log_count}') + # embed.add_field(name="Past logs", value=f"{log_count}") thread = "thread" if log_count == 1 else "threads" embed.description += f" with **{log_count or 'no'}** past {thread}." else: @@ -365,7 +356,7 @@ async def _close( embed.title = user event = "Thread Closed as Scheduled" if scheduled else "Thread Closed" - # embed.set_author(name=f'Event: {event}', url=log_url) + # embed.set_author(name=f"Event: {event}", url=log_url) embed.set_footer(text=f"{event} by {_closer}") embed.timestamp = datetime.utcnow() @@ -389,10 +380,7 @@ 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 @@ -408,9 +396,7 @@ async def _close( await asyncio.gather(*tasks) async def cancel_closure( - self, - auto_close: bool = False, - all: bool = False, # pylint: disable=redefined-builtin + self, auto_close: bool = False, all: bool = False # pylint: disable=redefined-builtin ) -> None: if self.close_task is not None and (not auto_close or all): self.close_task.cancel() @@ -433,9 +419,7 @@ async def _find_thread_message(channel, message_id): if str(message_id) == str(embed.author.url).split("/")[-1]: return msg - async def _fetch_timeout( - self - ) -> typing.Union[None, isodate.duration.Duration, timedelta]: + async def _fetch_timeout(self) -> typing.Union[None, isodate.duration.Duration, timedelta]: """ This grabs the timeout value for closing threads automatically from the ConfigManager and parses it for use internally. @@ -481,10 +465,7 @@ async def _restart_close_timer(self): ) await self.close( - closer=self.bot.user, - after=int(seconds), - message=close_message, - auto_close=True, + closer=self.bot.user, after=int(seconds), message=close_message, auto_close=True ) async def edit_message(self, message_id: int, message: str) -> None: @@ -557,19 +538,12 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None else: # Send the same thing in the thread channel. tasks.append( - self.send( - message, - destination=self.channel, - from_mod=True, - anonymous=anonymous, - ) + self.send(message, destination=self.channel, from_mod=True, anonymous=anonymous) ) tasks.append( self.bot.api.append_log( - message, - self.channel.id, - type_="anonymous" if anonymous else "thread_message", + message, self.channel.id, type_="anonymous" if anonymous else "thread_message" ) ) @@ -626,16 +600,10 @@ async def send( embed = discord.Embed(description=message.content, timestamp=message.created_at) - system_avatar_url = ( - "https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png" - ) + system_avatar_url = "https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png" if not note: - if ( - anonymous - and from_mod - and not isinstance(destination, discord.TextChannel) - ): + if anonymous and from_mod and not isinstance(destination, discord.TextChannel): # Anonymously sending to the user. tag = self.bot.config["mod_tag"] if tag is None: @@ -656,9 +624,7 @@ async def send( 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}", + name=name, icon_url=avatar_url, url=f"https://discordapp.com/users/{author.id}" ) else: # Special note messages @@ -694,9 +660,7 @@ async def send( additional_count = 1 for url, filename in images: - if not prioritize_uploads or ( - is_image_url(url) and not embedded_image and filename - ): + if not prioritize_uploads or (is_image_url(url) and not embedded_image and filename): embed.set_image(url=url) if filename: embed.add_field(name="Image", value=f"[{filename}]({url})") @@ -713,9 +677,7 @@ async def send( img_embed.set_image(url=url) img_embed.title = filename img_embed.url = url - img_embed.set_footer( - text=f"Additional Image Upload ({additional_count})" - ) + img_embed.set_footer(text=f"Additional Image Upload ({additional_count})") img_embed.timestamp = message.created_at additional_images.append(destination.send(embed=img_embed)) additional_count += 1 @@ -755,14 +717,8 @@ async def send( except Exception as 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 - ) + 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() @@ -835,8 +791,7 @@ async def find( thread = self._find_from_channel(channel) if thread is None: user_id, thread = next( - ((k, v) for k, v in self.cache.items() if v.channel == channel), - (-1, None), + ((k, v) for k, v in self.cache.items() if v.channel == channel), (-1, None) ) if thread is not None: logger.debug("Found thread with tempered ID.") @@ -852,9 +807,7 @@ async def find( thread = self.cache[recipient_id] if not thread.channel or not self.bot.get_channel(thread.channel.id): self.bot.loop.create_task( - thread.close( - closer=self.bot.user, silent=True, delete_channel=False - ) + thread.close(closer=self.bot.user, silent=True, delete_channel=False) ) thread = None except KeyError: @@ -916,8 +869,7 @@ def format_channel_name(self, author): """Sanitises a username for use with text channel names""" name = author.name.lower() new_name = ( - "".join(l for l in name if l not in string.punctuation and l.isprintable()) - or "null" + "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" ) new_name += f"-{author.discriminator}" diff --git a/core/time.py b/core/time.py index ba27fa021c..f10892ec62 100644 --- a/core/time.py +++ b/core/time.py @@ -58,10 +58,7 @@ def __init__(self, argument): if not status.hasTime: # replace it with the current time dt = dt.replace( - hour=now.hour, - minute=now.minute, - second=now.second, - microsecond=now.microsecond, + hour=now.hour, minute=now.minute, second=now.second, microsecond=now.microsecond ) self.dt = dt diff --git a/core/utils.py b/core/utils.py index b62917119c..68c0870c71 100644 --- a/core/utils.py +++ b/core/utils.py @@ -20,7 +20,7 @@ def strtobool(val): val = val.lower() if val == "enable": return 1 - elif val == "disable": + if val == "disable": return 0 raise @@ -206,8 +206,7 @@ def match_user_id(text: str) -> int: 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.**", + color=discord.Color.red(), description=f"**{name.capitalize()} `{word}` cannot be found.**" ) val = get_close_matches(word, possibilities, n=n, cutoff=cutoff) if val: diff --git a/pyproject.toml b/pyproject.toml index 42d03a7617..c2d008bbc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,8 @@ [tool.black] -line-length = 88 -target-version = ['py37'] +line-length = 99 +target-version = ['py36'] include = '\.pyi?$' exclude = ''' - ( /( \.eggs From 14531086ea71bc2bc465a63344ae725d84f1b644 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 7 Nov 2019 02:44:27 -0800 Subject: [PATCH 03/38] Improved multicommand alias v3.3.1-dev1 --- CHANGELOG.md | 4 +- bot.py | 2 +- cogs/utility.py | 151 ++++++++++++++++++++---------------------------- core/utils.py | 40 +++++-------- 4 files changed, 81 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f31846610f..b536a4808e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,18 @@ 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.1-dev0 +# v3.3.1-dev1 ### Added - "enable" and "disable" support for yes or no config vars. - Added "perhaps you meant" section to `?config help`. +- Multi-command alias is now more stable. With support for a single quote escape `\"`. ### Internal - Commit to black format line width max = 99, consistent with pylint. +- Alias parser is rewritten without shlex. # v3.3.0 diff --git a/bot.py b/bot.py index 48910db578..1589ae78ac 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.1-dev0" +__version__ = "3.3.1-dev1" import asyncio import logging diff --git a/cogs/utility.py b/cogs/utility.py index 392656b650..f87b0f552d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -16,6 +16,7 @@ import discord from discord.enums import ActivityType, Status from discord.ext import commands, tasks +from discord.ext.commands.view import StringView from discord.utils import escape_markdown, escape_mentions from aiohttp import ClientResponseError @@ -1002,6 +1003,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) values = utils.parse_alias(value) + save_aliases = [] if not values: embed = discord.Embed( @@ -1012,59 +1014,45 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') return await ctx.send(embed=embed) - if len(values) == 1: - linked_command, *messages = values[0].split(maxsplit=1) + multiple_alias = len(values) > 1 + + embed = discord.Embed( + title="Added alias", + color=self.bot.main_color + ) + + if multiple_alias: + embed.description = f'`{name}` points to: "{values[0]}".' + else: + embed.description = f"`{name}` now points to the following steps:" + + for i, val in enumerate(values, start=1): + view = StringView(val) + linked_command = view.get_word() + message = view.read_rest() + if not self.bot.get_command(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] + save_aliases.append(f"{alias_command} {message}".strip()) 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="Error", color=self.bot.error_color) - embed = discord.Embed( - title="Added alias", - color=self.bot.main_color, - description=f'`{name}` points to: "{values[0]}".', - ) + if multiple_alias: + embed.description = ("The command you are attempting to point " + f"to does not exist: `{linked_command}`.") + else: + embed.description = ("The command you are attempting to point " + f"to n step {i} does not exist: `{linked_command}`.") - else: - embed = discord.Embed( - title="Added alias", - color=self.bot.main_color, - description=f"`{name}` now points to the following steps:", - ) + return await ctx.send(embed=embed) + else: + save_aliases.append(val) - for i, val in enumerate(values, start=1): - linked_command, *messages = val.split(maxsplit=1) - if not self.bot.get_command(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 n step {i} does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) - embed.description += f"\n{i}: {val}" + embed.description += f"\n{i}: {val}" - self.bot.aliases[name] = " && ".join(values) + self.bot.aliases[name] = " && ".join(f"\"{a}\"" for a in save_aliases) await self.bot.config.update() - return await ctx.send(embed=embed) @alias.command(name="remove", aliases=["del", "delete"]) @@ -1097,6 +1085,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) values = utils.parse_alias(value) + save_aliases = [] if not values: embed = discord.Embed( @@ -1107,56 +1096,44 @@ async def alias_edit(self, ctx, name: str.lower, *, value): embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') return await ctx.send(embed=embed) - if len(values) == 1: - linked_command, *messages = values[0].split(maxsplit=1) + multiple_alias = len(values) > 1 + + embed = discord.Embed( + title="Edited alias", + color=self.bot.main_color + ) + + if multiple_alias: + embed.description = f'`{name}` points to: "{values[0]}".' + else: + embed.description = f"`{name}` now points to the following steps:" + + for i, val in enumerate(values, start=1): + view = StringView(val) + linked_command = view.get_word() + message = view.read_rest() + if not self.bot.get_command(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] + save_aliases.append(f"{alias_command} {message}".strip()) 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, - description=f'`{name}` now points to: "{values[0]}".', - ) + embed = discord.Embed(title="Error", color=self.bot.error_color) - else: - embed = discord.Embed( - title="Edited alias", - color=self.bot.main_color, - description=f"`{name}` now points to the following steps:", - ) - - for i, val in enumerate(values, start=1): - linked_command, *messages = val.split(maxsplit=1) - if not self.bot.get_command(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] + if multiple_alias: + embed.description = ("The command you are attempting to point " + f"to does not exist: `{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}" + embed.description = ("The command you are attempting to point " + f"to n step {i} does not exist: `{linked_command}`.") + + return await ctx.send(embed=embed) + else: + save_aliases.append(val) + + embed.description += f"\n{i}: {val}" - self.bot.aliases[name] = "&&".join(values) + self.bot.aliases[name] = " && ".join(f"\"{a}\"" for a in save_aliases) await self.bot.config.update() return await ctx.send(embed=embed) diff --git a/core/utils.py b/core/utils.py index 68c0870c71..23a89428b1 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,6 +1,6 @@ +import base64 import functools import re -import shlex import typing from difflib import get_close_matches from distutils.util import strtobool as _stb # pylint: disable=import-error @@ -215,35 +215,21 @@ def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discor def parse_alias(alias): - if "&&" not in alias: - if alias.startswith('"') and alias.endswith('"'): - return [alias[1:-1]] - return [alias] + def encode_alias(m): + return "\x1AU" + base64.b64encode(m.group(1).encode()).decode() + "\x1AU" - buffer = "" - cmd = [] - try: - for token in shlex.shlex(alias, punctuation_chars="&"): - if token != "&&": - buffer += " " + token - continue - - buffer = buffer.strip() - if buffer.startswith('"') and buffer.endswith('"'): - buffer = buffer[1:-1] - cmd += [buffer] - buffer = "" - except ValueError: - return [] + def decode_alias(m): + return base64.b64decode(m.group(1).encode()).decode() + + alias = re.sub(r"(?:(?<=^)(?:\s*(? Date: Thu, 7 Nov 2019 10:55:56 -0800 Subject: [PATCH 04/38] Fixed some issues with alias from last commit --- bot.py | 36 ++++++++++++----------------------- cogs/utility.py | 50 +++++++++++++++++++++++++------------------------ core/utils.py | 28 +++++++++++++++++++++++---- 3 files changed, 62 insertions(+), 52 deletions(-) diff --git a/bot.py b/bot.py index 1589ae78ac..cde9b247fa 100644 --- a/bot.py +++ b/bot.py @@ -34,7 +34,7 @@ from core import checks from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import human_join, parse_alias +from core.utils import human_join, parse_alias, normalize_alias from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging from core.thread import ThreadManager from core.time import human_timedelta @@ -723,32 +723,20 @@ async def get_contexts(self, message, *, cls=commands.Context): # Check if there is any aliases being called. alias = self.aliases.get(invoker) if alias is not None: - aliases = parse_alias(alias) + ctxs = [] + aliases = normalize_alias(alias, message.content[len(f"{invoked_prefix}{invoker}") :]) if not aliases: logger.warning("Alias %s is invalid, removing.", invoker) self.aliases.pop(invoker) - else: - len_ = len(f"{invoked_prefix}{invoker}") - contents = parse_alias(message.content[len_:]) - if not contents: - contents = [message.content[len_:]] - - ctxs = [] - for alias, content in zip_longest(aliases, contents): - if alias is None: - break - ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) - ctx.thread = await self.threads.find(channel=ctx.channel) - - if content is not None: - view = StringView(f"{alias} {content.strip()}") - else: - view = StringView(alias) - ctx.view = view - ctx.invoked_with = view.get_word() - ctx.command = self.all_commands.get(ctx.invoked_with) - ctxs += [ctx] - return ctxs + for alias in aliases: + view = StringView(alias) + ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) + ctx.thread = await self.threads.find(channel=ctx.channel) + ctx.view = view + ctx.invoked_with = view.get_word() + ctx.command = self.all_commands.get(ctx.invoked_with) + ctxs += [ctx] + return ctxs ctx.invoked_with = invoker ctx.command = self.all_commands.get(invoker) diff --git a/cogs/utility.py b/cogs/utility.py index f87b0f552d..84637ae427 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1016,34 +1016,35 @@ async def alias_add(self, ctx, name: str.lower, *, value): multiple_alias = len(values) > 1 - embed = discord.Embed( - title="Added alias", - color=self.bot.main_color - ) + embed = discord.Embed(title="Added alias", color=self.bot.main_color) - if multiple_alias: + if not multiple_alias: embed.description = f'`{name}` points to: "{values[0]}".' else: embed.description = f"`{name}` now points to the following steps:" for i, val in enumerate(values, start=1): view = StringView(val) - linked_command = view.get_word() + linked_command = view.get_word().lower() message = view.read_rest() if not self.bot.get_command(linked_command): alias_command = self.bot.aliases.get(linked_command) if alias_command is not None: - save_aliases.append(f"{alias_command} {message}".strip()) + save_aliases.extend(utils.normalize_alias(alias_command, message)) else: embed = discord.Embed(title="Error", color=self.bot.error_color) if multiple_alias: - embed.description = ("The command you are attempting to point " - f"to does not exist: `{linked_command}`.") + embed.description = ( + "The command you are attempting to point " + f"to does not exist: `{linked_command}`." + ) else: - embed.description = ("The command you are attempting to point " - f"to n step {i} does not exist: `{linked_command}`.") + embed.description = ( + "The command you are attempting to point " + f"to n step {i} does not exist: `{linked_command}`." + ) return await ctx.send(embed=embed) else: @@ -1051,7 +1052,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed.description += f"\n{i}: {val}" - self.bot.aliases[name] = " && ".join(f"\"{a}\"" for a in save_aliases) + self.bot.aliases[name] = " && ".join(f'"{a}"' for a in save_aliases) await self.bot.config.update() return await ctx.send(embed=embed) @@ -1098,34 +1099,35 @@ async def alias_edit(self, ctx, name: str.lower, *, value): multiple_alias = len(values) > 1 - embed = discord.Embed( - title="Edited alias", - color=self.bot.main_color - ) + embed = discord.Embed(title="Edited alias", color=self.bot.main_color) - if multiple_alias: + if not multiple_alias: embed.description = f'`{name}` points to: "{values[0]}".' else: embed.description = f"`{name}` now points to the following steps:" for i, val in enumerate(values, start=1): view = StringView(val) - linked_command = view.get_word() + linked_command = view.get_word().lower() message = view.read_rest() if not self.bot.get_command(linked_command): alias_command = self.bot.aliases.get(linked_command) if alias_command is not None: - save_aliases.append(f"{alias_command} {message}".strip()) + save_aliases.extend(utils.normalize_alias(alias_command, message)) else: embed = discord.Embed(title="Error", color=self.bot.error_color) if multiple_alias: - embed.description = ("The command you are attempting to point " - f"to does not exist: `{linked_command}`.") + embed.description = ( + "The command you are attempting to point " + f"to does not exist: `{linked_command}`." + ) else: - embed.description = ("The command you are attempting to point " - f"to n step {i} does not exist: `{linked_command}`.") + embed.description = ( + "The command you are attempting to point " + f"to n step {i} does not exist: `{linked_command}`." + ) return await ctx.send(embed=embed) else: @@ -1133,7 +1135,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value): embed.description += f"\n{i}: {val}" - self.bot.aliases[name] = " && ".join(f"\"{a}\"" for a in save_aliases) + self.bot.aliases[name] = " && ".join(f'"{a}"' for a in save_aliases) await self.bot.config.update() return await ctx.send(embed=embed) diff --git a/core/utils.py b/core/utils.py index 23a89428b1..44b890f230 100644 --- a/core/utils.py +++ b/core/utils.py @@ -4,7 +4,7 @@ import typing from difflib import get_close_matches from distutils.util import strtobool as _stb # pylint: disable=import-error -from itertools import takewhile +from itertools import takewhile, zip_longest from urllib import parse import discord @@ -221,9 +221,12 @@ def encode_alias(m): def decode_alias(m): return base64.b64decode(m.group(1).encode()).decode() - alias = re.sub(r"(?:(?<=^)(?:\s*(? Date: Thu, 7 Nov 2019 10:56:48 -0800 Subject: [PATCH 05/38] Unused imports --- bot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot.py b/bot.py index cde9b247fa..27c8e73e2b 100644 --- a/bot.py +++ b/bot.py @@ -7,7 +7,6 @@ import sys import typing from datetime import datetime -from itertools import zip_longest from types import SimpleNamespace import discord @@ -34,7 +33,7 @@ from core import checks from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import human_join, parse_alias, normalize_alias +from core.utils import human_join, normalize_alias from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging from core.thread import ThreadManager from core.time import human_timedelta From 1fa212acdcb0cc6442da8a9fbede28b8d8b35a90 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 12 Nov 2019 01:07:30 -0800 Subject: [PATCH 06/38] v3.3.1-dev2 --- CHANGELOG.md | 9 +- bot.py | 275 +++++++++++++++++++++++++----------------- core/clients.py | 7 ++ core/config.py | 39 +++--- core/config_help.json | 11 ++ core/thread.py | 22 ++-- core/time.py | 37 +++--- 7 files changed, 237 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b536a4808e..4e3ea1a70b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,21 @@ 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.1-dev1 +# v3.3.1-dev2 ### Added +- Thread cooldown! + - Set via the new config var `thread_cooldown`. + - Specify a time for the recipient to wait before allowed to create another thread. - "enable" and "disable" support for yes or no config vars. - Added "perhaps you meant" section to `?config help`. - Multi-command alias is now more stable. With support for a single quote escape `\"`. +### Fixed + +- Setting config vars using human time wasn't working. + ### Internal - Commit to black format line width max = 99, consistent with pylint. diff --git a/bot.py b/bot.py index 27c8e73e2b..5e578cac7d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.1-dev1" +__version__ = "3.3.1-dev2" import asyncio import logging @@ -529,122 +529,171 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: return sent_emoji, blocked_emoji - 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: - if str(message.author.id) in self.blocked_users: - self.blocked_users.pop(str(message.author.id)) - await self.config.update() - - return False, sent_emoji - - now = datetime.utcnow() - + def check_account_age(self, author: discord.Member) -> bool: 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() - - reason = self.blocked_users.get(str(message.author.id)) or "" - min_guild_age = min_account_age = now + now = datetime.utcnow() try: - min_account_age = message.author.created_at + account_age + min_account_age = author.created_at + account_age except ValueError: logger.warning("Error with 'account_age'.", exc_info=True) - self.config.remove("account_age") - - try: - joined_at = getattr(message.author, "joined_at", None) - if joined_at is not None: - min_guild_age = joined_at + guild_age - except ValueError: - logger.warning("Error with 'guild_age'.", exc_info=True) - self.config.remove("guild_age") + min_account_age = author.created_at + self.config.remove("account_age") if min_account_age > now: # User account has not reached the required time - reaction = blocked_emoji - changed = False delta = human_timedelta(min_account_age) - logger.debug("Blocked due to account age, user %s.", message.author.name) + logger.debug("Blocked due to account age, user %s.", author.name) - if str(message.author.id) not in self.blocked_users: + if str(author.id) not in self.blocked_users: new_reason = f"System Message: New Account. Required to wait for {delta}." - self.blocked_users[str(message.author.id)] = new_reason - changed = True + self.blocked_users[str(author.id)] = new_reason - if reason.startswith("System Message: New Account.") or changed: - await message.channel.send( - embed=discord.Embed( - title="Message not sent!", - description=f"Your must wait for {delta} before you can contact me.", - color=self.error_color, - ) - ) + return False + return True + + def check_guild_age(self, author: discord.Member) -> bool: + guild_age = self.config.get("guild_age") + now = datetime.utcnow() + + if not hasattr(author, "joined_at"): + logger.warning("Not in guild, cannot verify guild_age, %s.", author.name) + return True - elif min_guild_age > now: + try: + min_guild_age = author.joined_at + guild_age + except ValueError: + logger.warning("Error with 'guild_age'.", exc_info=True) + min_guild_age = author.joined_at + self.config.remove("guild_age") + + if min_guild_age > now: # User has not stayed in the guild for long enough - reaction = blocked_emoji - changed = False delta = human_timedelta(min_guild_age) - logger.debug("Blocked due to guild age, user %s.", message.author.name) + logger.debug("Blocked due to guild age, user %s.", author.name) - if str(message.author.id) not in self.blocked_users: + if str(author.id) not in self.blocked_users: new_reason = f"System Message: Recently Joined. Required to wait for {delta}." - self.blocked_users[str(message.author.id)] = new_reason - changed = True + self.blocked_users[str(author.id)] = new_reason - if reason.startswith("System Message: Recently Joined.") or changed: - await message.channel.send( - embed=discord.Embed( - title="Message not sent!", - description=f"Your must wait for {delta} before you can contact me.", - color=self.error_color, - ) + return False + return True + + def check_manual_blocked(self, author: discord.Member) -> bool: + if str(author.id) not in self.blocked_users: + return True + + blocked_reason = self.blocked_users.get(str(author.id)) or "" + now = datetime.utcnow() + + if blocked_reason.startswith("System Message:"): + # Met the limits already, otherwise it would've been caught by the previous checks + logger.debug("No longer internally blocked, user %s.", author.name) + self.blocked_users.pop(str(author.id)) + return True + # etc "blah blah blah... until 2019-10-14T21:12:45.559948." + end_time = re.search(r"until ([^`]+?)\.$", blocked_reason) + if end_time is None: + # backwards compat + end_time = re.search(r"%([^%]+?)%", blocked_reason) + if end_time is not None: + logger.warning( + r"Deprecated time message for user %s, block and unblock again to update.", + author.name, ) - elif str(message.author.id) in self.blocked_users: - if reason.startswith("System Message: New Account.") or reason.startswith( - "System Message: Recently Joined." - ): - # Met the age limit already, otherwise it would've been caught by the previous if's - reaction = sent_emoji - logger.debug("No longer internally blocked, user %s.", message.author.name) - self.blocked_users.pop(str(message.author.id)) - else: - reaction = blocked_emoji - # 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: + after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() + if after <= 0: + # No longer blocked + self.blocked_users.pop(str(author.id)) + logger.debug("No longer blocked, user %s.", author.name) + return True + logger.debug("User blocked, user %s.", author.name) + return False + + async def _process_blocked(self, message): + sent_emoji, blocked_emoji = await self.retrieve_emoji() + if await self.is_blocked(message.author, channel=message.channel, send_message=True): + await self.add_reaction(message, blocked_emoji) + return True + return False + + async def is_blocked( + self, + author: discord.User, + *, + channel: discord.TextChannel = None, + send_message: bool = False, + ) -> typing.Tuple[bool, str]: + + member = self.guild.get_member(author.id) + if member is None: + logger.debug("User not in guild, %s.", author.id) + else: + author = member + + if str(author.id) in self.blocked_whitelisted_users: + if str(author.id) in self.blocked_users: + self.blocked_users.pop(str(author.id)) + await self.config.update() + return False + + blocked_reason = self.blocked_users.get(str(author.id)) or "" + + if ( + not self.check_account_age(author) + or not self.check_guild_age(author) + ): + new_reason = self.blocked_users.get(str(author.id)) + if new_reason != blocked_reason: + if send_message: + await channel.send( + embed=discord.Embed( + title="Message not sent!", + description=new_reason, + color=self.error_color, ) + ) + return True - if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() - if after <= 0: - # 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: - reaction = sent_emoji + if not self.check_manual_blocked(author): + return True await self.config.update() - return str(message.author.id) in self.blocked_users, reaction + return False + + async def get_thread_cooldown(self, author: discord.Member): + thread_cooldown = self.config.get("thread_cooldown") + now = datetime.utcnow() + + if thread_cooldown == isodate.Duration(): + return + + last_log = await self.api.get_latest_user_logs(author.id) + + if last_log is None: + logger.debug("Last thread wasn't found, %s.", author.name) + return + + last_log_closed_at = last_log.get("closed_at") + + if not last_log_closed_at: + logger.debug("Last thread was not closed, %s.", author.name) + return + + try: + cooldown = datetime.fromisoformat(last_log_closed_at) + thread_cooldown + except ValueError: + logger.warning("Error with 'thread_cooldown'.", exc_info=True) + cooldown = datetime.fromisoformat(last_log_closed_at) + self.config.remove( + "thread_cooldown" + ) + + if cooldown > now: + # User messaged before thread cooldown ended + delta = human_timedelta(cooldown) + logger.debug("Blocked due to thread cooldown, user %s.", author.name) + return delta + return @staticmethod async def add_reaction(msg, reaction): @@ -656,11 +705,24 @@ async def add_reaction(msg, reaction): async def process_dm_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" - blocked, reaction = await self._process_blocked(message) + blocked = await self._process_blocked(message) if blocked: - return await self.add_reaction(message, reaction) + return + sent_emoji, blocked_emoji = await self.retrieve_emoji() + thread = await self.threads.find(recipient=message.author) if thread is None: + delta = await self.get_thread_cooldown(message.author) + if delta: + await message.channel.send( + embed=discord.Embed( + title="Message not sent!", + description=f"You must wait for {delta} before you can contact me again.", + color=self.error_color, + ) + ) + return + if self.config["dm_disabled"] >= 1: embed = discord.Embed( title=self.config["disabled_new_thread_title"], @@ -673,9 +735,9 @@ async def process_dm_modmail(self, message: discord.Message) -> None: 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: @@ -691,12 +753,16 @@ async def process_dm_modmail(self, message: discord.Message) -> None: 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) + try: + await thread.send(message) + except Exception: + logger.error("Failed to send message:", exc_info=True) + await self.add_reaction(message, blocked_emoji) + else: + await self.add_reaction(message, sent_emoji) async def get_contexts(self, message, *, cls=commands.Context): """ @@ -849,9 +915,6 @@ async def on_typing(self, channel, user, _): if user.bot: return - async def _void(*_args, **_kwargs): - pass - if isinstance(channel, discord.DMChannel): if not self.config.get("user_typing"): return @@ -866,13 +929,7 @@ 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) - ) - ) - )[0]: + if await self.is_blocked(thread.recipient): return await thread.recipient.trigger_typing() diff --git a/core/clients.py b/core/clients.py index 7bc17943e5..d8814335bf 100644 --- a/core/clients.py +++ b/core/clients.py @@ -91,6 +91,13 @@ 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_latest_user_logs(self, user_id: Union[str, int]): + query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id), "open": False} + projection = {"messages": {"$slice": 5}} + logger.debug("Retrieving user %s latest logs.", user_id) + + return await self.logs.find_one(query, projection, limit=1, sort=[("closed_at", -1)]) + async def get_responded_logs(self, user_id: Union[str, int]) -> list: query = { "open": False, diff --git a/core/config.py b/core/config.py index ec8e66936d..695b743f56 100644 --- a/core/config.py +++ b/core/config.py @@ -13,7 +13,7 @@ from core._color_data import ALL_COLORS from core.models import InvalidConfigError, Default, getLogger -from core.time import UserFriendlyTime +from core.time import UserFriendlyTimeSync from core.utils import strtobool logger = getLogger(__name__) @@ -33,8 +33,9 @@ class ConfigManager: "error_color": str(discord.Color.red()), "user_typing": False, "mod_typing": False, - "account_age": None, - "guild_age": None, + "account_age": isodate.Duration(), + "guild_age": isodate.Duration(), + "thread_cooldown": isodate.Duration(), "reply_without_command": False, "anon_reply_without_command": False, # logging @@ -45,7 +46,7 @@ class ConfigManager: "close_emoji": "🔒", "recipient_thread_close": False, "thread_auto_close_silently": False, - "thread_auto_close": None, + "thread_auto_close": isodate.Duration(), "thread_auto_close_response": "This thread has been closed automatically due to inactivity after {timeout}.", "thread_creation_response": "The staff team will get back to you as soon as possible.", "thread_creation_footer": "Your message has been sent", @@ -115,7 +116,7 @@ class ConfigManager: colors = {"mod_color", "recipient_color", "main_color", "error_color"} - time_deltas = {"account_age", "guild_age", "thread_auto_close"} + time_deltas = {"account_age", "guild_age", "thread_auto_close", "thread_cooldown"} booleans = { "user_typing", @@ -224,17 +225,16 @@ def get(self, key: str, convert=True) -> typing.Any: value = int(self.remove(key).lstrip("#"), base=16) elif key in self.time_deltas: - if value is None: - return None - 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) + if not isinstance(value, isodate.Duration): + 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: @@ -298,13 +298,14 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: isodate.parse_duration(item) except isodate.ISO8601Error: try: - converter = UserFriendlyTime() - time = self.bot.loop.run_until_complete(converter.convert(None, item)) + converter = UserFriendlyTimeSync() + time = converter.convert(None, item) if time.arg: raise ValueError except BadArgument as exc: raise InvalidConfigError(*exc.args) - except Exception: + except Exception as e: + logger.debug(e) raise InvalidConfigError( "Unrecognized time, please use ISO-8601 duration format " 'string or a simpler "human readable" time.' diff --git a/core/config_help.json b/core/config_help.json index 0a6cee5076..c6c0fc95d0 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -222,6 +222,17 @@ "See also: `thread_auto_close_silently`, `thread_auto_close_response`." ] }, + "thread_cooldown": { + "default": "Never", + "description": "Specify the time required for the recipient to wait before allowed to create a new thread.", + "examples": [ + "`{prefix}config set thread_cooldown P12DT3H` (stands for 12 days and 3 hours in [ISO-8601 Duration Format](https://en.wikipedia.org/wiki/ISO_8601#Durations))", + "`{prefix}config set thread_cooldown 3 days and 5 hours` (accepted readable time)" + ], + "notes": [ + "To disable thread cooldown, do `{prefix}config del thread_cooldown`." + ] + }, "thread_auto_close_response": { "default": "\"This thread has been closed automatically due to inactivity after {{timeout}}.\"", "description": "This is the message to display when the thread when the thread auto-closes.", diff --git a/core/thread.py b/core/thread.py index 9a40c07fd8..bf13030776 100644 --- a/core/thread.py +++ b/core/thread.py @@ -295,8 +295,8 @@ async def _close( ): try: self.manager.cache.pop(self.id) - except KeyError: - logger.warning("Thread already closed.", exc_info=True) + except KeyError as e: + logger.error("Thread already closed: %s.", str(e)) return await self.cancel_closure(all=True) @@ -436,7 +436,7 @@ async def _restart_close_timer(self): timeout = await self._fetch_timeout() # Exit if timeout was not set - if not timeout: + if timeout == isodate.Duration(): return # Set timeout seconds @@ -723,8 +723,8 @@ async def send( try: await destination.trigger_typing() except discord.NotFound: - logger.warning("Channel not found.", exc_info=True) - return + logger.warning("Channel not found.") + raise if not from_mod and not note: mentions = self.get_notifications() @@ -804,12 +804,12 @@ async def find( recipient_id = recipient.id try: - thread = self.cache[recipient_id] - if not thread.channel or not self.bot.get_channel(thread.channel.id): - self.bot.loop.create_task( - thread.close(closer=self.bot.user, silent=True, delete_channel=False) - ) - thread = None + return self.cache[recipient_id] + # if not thread.channel or not self.bot.get_channel(thread.channel.id): + # self.bot.loop.create_task( + # thread.close(closer=self.bot.user, silent=True, delete_channel=False) + # ) + # thread = None except KeyError: channel = discord.utils.get( self.bot.modmail_guild.text_channels, topic=f"User ID: {recipient_id}" diff --git a/core/time.py b/core/time.py index f10892ec62..f91ad0dc09 100644 --- a/core/time.py +++ b/core/time.py @@ -84,32 +84,23 @@ def __init__(self, argument): raise BadArgument("The time is in the past.") -class UserFriendlyTime(Converter): +class UserFriendlyTimeSync(Converter): """That way quotes aren't absolutely necessary.""" - def __init__(self, converter: Converter = None): - if isinstance(converter, type) and issubclass(converter, Converter): - converter = converter() - - if converter is not None and not isinstance(converter, Converter): - raise TypeError("commands.Converter subclass necessary.") + def __init__(self): self.raw: str = None self.dt: datetime = None self.arg = None self.now: datetime = None - self.converter = converter - async def check_constraints(self, ctx, now, remaining): + def check_constraints(self, now, remaining): if self.dt < now: raise BadArgument("This time is in the past.") - if self.converter is not None: - self.arg = await self.converter.convert(ctx, remaining) - else: - self.arg = remaining + self.arg = remaining return self - async def convert(self, ctx, argument): + def convert(self, ctx, argument): self.raw = argument remaining = "" try: @@ -122,7 +113,7 @@ async def convert(self, ctx, argument): data = {k: int(v) for k, v in match.groupdict(default="0").items()} remaining = argument[match.end() :].strip() self.dt = self.now + relativedelta(**data) - return await self.check_constraints(ctx, self.now, remaining) + return self.check_constraints(self.now, remaining) # apparently nlp does not like "from now" # it likes "from x" in other cases though @@ -133,14 +124,9 @@ async def convert(self, ctx, argument): if argument.startswith("for "): argument = argument[4:].strip() - if argument[0:2] == "me": - # starts with "me to", "me in", or "me at " - if argument[0:6] in ("me to ", "me in ", "me at "): - argument = argument[6:] - elements = calendar.nlp(argument, sourceTime=self.now) if elements is None or not elements: - return await self.check_constraints(ctx, self.now, argument) + return self.check_constraints(self.now, argument) # handle the following cases: # "date time" foo @@ -151,7 +137,7 @@ async def convert(self, ctx, argument): dt, status, begin, end, _ = elements[0] if not status.hasDateOrTime: - return await self.check_constraints(ctx, self.now, argument) + return self.check_constraints(self.now, argument) if begin not in (0, 1) and end != len(argument): raise BadArgument( @@ -190,12 +176,17 @@ async def convert(self, ctx, argument): elif len(argument) == end: remaining = argument[:begin].strip() - return await self.check_constraints(ctx, self.now, remaining) + return self.check_constraints(self.now, remaining) except Exception: logger.exception("Something went wrong while parsing the time.") raise +class UserFriendlyTime(UserFriendlyTimeSync): + async def convert(self, ctx, argument): + return super().convert(ctx, argument) + + def human_timedelta(dt, *, source=None): now = source or datetime.utcnow() if dt > now: From c0c8db9810906e993c7b68b8731ff1d52a3990ea Mon Sep 17 00:00:00 2001 From: DAzVise Date: Fri, 15 Nov 2019 15:01:55 +0300 Subject: [PATCH 07/38] added fallback category --- bot.py | 2 +- core/config.py | 1 + core/config_help.json | 10 ++++++++++ core/thread.py | 20 ++++++++++++++++++-- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 5e578cac7d..89ac13bb1a 100644 --- a/bot.py +++ b/bot.py @@ -738,7 +738,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) - thread = self.threads.create(message.author) + thread = await self.threads.create(message.author) else: if self.config["dm_disabled"] == 2: embed = discord.Embed( diff --git a/core/config.py b/core/config.py index 695b743f56..a0eda4a2dc 100644 --- a/core/config.py +++ b/core/config.py @@ -27,6 +27,7 @@ class ConfigManager: "twitch_url": "https://www.twitch.tv/discordmodmail/", # bot settings "main_category_id": None, + "fallback_category_id": None, "prefix": "?", "mention": "@here", "main_color": str(discord.Color.blurple()), diff --git a/core/config_help.json b/core/config_help.json index c6c0fc95d0..177e488094 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -20,6 +20,16 @@ "If the Modmail category ended up being non-existent/invalid, Modmail will break. To fix this, run `{prefix}setup` again or set `main_category_id` to a valid category." ] }, + "fallback_category_id": { + "default": "`Fallback Modmail` (created when the main category is full)", + "description": "This is the category that will hold the threads when the main category is full.\n\nTo change the Fallback category, you will need to find the [category’s ID](https://support.discordapp.com/hc/en-us/articles/206346498).", + "examples": [ + "`{prefix}config set fallback_category_id 9234932582312` (`9234932582312` is the category ID)" + ], + "notes": [ + "If the Fallback category ended up being non-existent/invalid, Modmail will create a new one. To fix this, set `fallback_category_id` to a valid category." + ] + }, "prefix": { "default": "`?`", "description": "The prefix of the bot.", diff --git a/core/thread.py b/core/thread.py index bf13030776..b6dac99eaf 100644 --- a/core/thread.py +++ b/core/thread.py @@ -846,7 +846,7 @@ def _find_from_channel(self, channel): return thread return None - def create( + async def create( self, recipient: typing.Union[discord.Member, discord.User], *, @@ -859,11 +859,27 @@ def create( self.cache[recipient.id] = thread # Schedule thread setup for later + cat = self.bot.main_category + if len(cat.channels) == 50: + fallback_id = self.bot.config["fallback_category_id"] + fallback = discord.utils.get(cat.guild.categories, id=int(fallback_id)) + if fallback and len(fallback.channels) != 50: + self.bot.loop.create_task(thread.setup(creator=creator, category=fallback)) + return thread + + fallback = await cat.clone(name="Fallback Modmail") + self.bot.config.set("fallback_category_id", fallback.id) + await self.bot.config.update() + self.bot.loop.create_task(thread.setup(creator=creator, category=fallback)) + return thread + + self.bot.loop.create_task(thread.setup(creator=creator, category=category)) + return thread self.bot.loop.create_task(thread.setup(creator=creator, category=category)) return thread async def find_or_create(self, recipient) -> Thread: - return await self.find(recipient=recipient) or self.create(recipient) + return await self.find(recipient=recipient) or await self.create(recipient) def format_channel_name(self, author): """Sanitises a username for use with text channel names""" From 9e0e6cac4df4a0e85f68500c5e449340ee50575d Mon Sep 17 00:00:00 2001 From: DAzVise <52792999+DAzVise@users.noreply.github.com> Date: Fri, 15 Nov 2019 21:03:54 +0300 Subject: [PATCH 08/38] whoops --- core/thread.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index b6dac99eaf..0e6d43e12a 100644 --- a/core/thread.py +++ b/core/thread.py @@ -875,8 +875,6 @@ async def create( self.bot.loop.create_task(thread.setup(creator=creator, category=category)) return thread - self.bot.loop.create_task(thread.setup(creator=creator, category=category)) - return thread async def find_or_create(self, recipient) -> Thread: return await self.find(recipient=recipient) or await self.create(recipient) From b41acc734430211fe79354886dbaa1d9f6c474dc Mon Sep 17 00:00:00 2001 From: DAzVise Date: Sat, 16 Nov 2019 11:19:57 +0300 Subject: [PATCH 09/38] black formatting --- bot.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bot.py b/bot.py index 89ac13bb1a..af313d2e85 100644 --- a/bot.py +++ b/bot.py @@ -639,10 +639,7 @@ async def is_blocked( blocked_reason = self.blocked_users.get(str(author.id)) or "" - if ( - not self.check_account_age(author) - or not self.check_guild_age(author) - ): + if not self.check_account_age(author) or not self.check_guild_age(author): new_reason = self.blocked_users.get(str(author.id)) if new_reason != blocked_reason: if send_message: From 2640053f0caf44a6039b6b9d7558744e72da3606 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 4 Nov 2019 01:10:41 -0800 Subject: [PATCH 10/38] strtobool support enable and disable, perhaps you meant for config help --- CHANGELOG.md | 7 +++++++ bot.py | 2 +- cogs/utility.py | 8 ++++++++ core/utils.py | 10 +++++++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b853cd5e..40f1e28db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +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). +# v3.3.1-dev0 + +### Added + +- "enable" and "disable" support for yes or no config vars. +- Added "perhaps you meant" section to `?config help`. + # v3.3.0 diff --git a/bot.py b/bot.py index 8a0093246e..b9a663f910 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0" +__version__ = "3.3.1-dev0" import asyncio import logging diff --git a/cogs/utility.py b/cogs/utility.py index 9610582334..b540483174 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -848,11 +848,19 @@ 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 ): + closest = get_close_matches( + key, {**self.bot.config.public_keys, **self.bot.config.protected_keys} + ) embed = discord.Embed( title="Error", color=self.bot.error_color, description=f"`{key}` is an invalid key.", ) + if closest: + embed.add_field( + name=f"Perhaps you meant:", + value="\n".join(f"`{x}`" for x in closest), + ) return await ctx.send(embed=embed) config_help = self.bot.config.config_help diff --git a/core/utils.py b/core/utils.py index fd85f0d5b0..b62917119c 100644 --- a/core/utils.py +++ b/core/utils.py @@ -14,7 +14,15 @@ def strtobool(val): if isinstance(val, bool): return val - return _stb(str(val)) + try: + return _stb(str(val)) + except ValueError: + val = val.lower() + if val == "enable": + return 1 + elif val == "disable": + return 0 + raise class User(commands.IDConverter): From e699865139c664752de73490fd55fab8a1e1ba18 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 4 Nov 2019 02:12:44 -0800 Subject: [PATCH 11/38] Reformat code --- .pylintrc | 2 +- .travis.yml | 1 + CHANGELOG.md | 4 + bot.py | 93 +++++++--------------- cogs/modmail.py | 126 +++++++++--------------------- cogs/plugins.py | 75 ++++++------------ cogs/utility.py | 185 ++++++++++++-------------------------------- core/_color_data.py | 4 +- core/changelog.py | 17 +--- core/checks.py | 4 +- core/clients.py | 25 ++---- core/config.py | 24 ++---- core/models.py | 15 ++-- core/paginator.py | 24 +++--- core/thread.py | 92 ++++++---------------- core/time.py | 5 +- core/utils.py | 5 +- pyproject.toml | 5 +- 18 files changed, 210 insertions(+), 496 deletions(-) diff --git a/.pylintrc b/.pylintrc index a45837fa82..21087a91f7 100644 --- a/.pylintrc +++ b/.pylintrc @@ -267,7 +267,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=99 # Maximum number of lines in a module. max-module-lines=1000 diff --git a/.travis.yml b/.travis.yml index 21035e6f3a..4f7ef0508f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,3 +27,4 @@ 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 + - pipenv run black . --check \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f1e28db0..6e2cb7b1a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ however, insignificant breaking changes does not guarantee a major version bump, - "enable" and "disable" support for yes or no config vars. - Added "perhaps you meant" section to `?config help`. +### Internal + +- Commit to black format line width max = 99, consistent with pylint. + # v3.3.0 diff --git a/bot.py b/bot.py index b9a663f910..48910db578 100644 --- a/bot.py +++ b/bot.py @@ -19,9 +19,10 @@ 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 +from pkg_resources import parse_version + try: # noinspection PyUnresolvedReferences from colorama import init @@ -171,9 +172,7 @@ def run(self, *args, **kwargs): for task in asyncio.all_tasks(self.loop): task.cancel() try: - self.loop.run_until_complete( - asyncio.gather(*asyncio.all_tasks(self.loop)) - ) + self.loop.run_until_complete(asyncio.gather(*asyncio.all_tasks(self.loop))) except asyncio.CancelledError: logger.debug("All pending tasks has been cancelled.") finally: @@ -187,9 +186,7 @@ def owner_ids(self): owner_ids = set(map(int, str(owner_ids).split(","))) if self.owner_id is not None: owner_ids.add(self.owner_id) - permissions = self.config["level_permissions"].get( - PermissionLevel.OWNER.name, [] - ) + permissions = self.config["level_permissions"].get(PermissionLevel.OWNER.name, []) for perm in permissions: owner_ids.add(int(perm)) return owner_ids @@ -216,8 +213,7 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: channel = self.main_category.channels[0] self.config["log_channel_id"] = channel.id logger.warning( - "No log channel set, setting #%s to be the log channel.", - channel.name, + "No log channel set, setting #%s to be the log channel.", channel.name ) return channel except IndexError: @@ -302,9 +298,7 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]: category_id = self.config["main_category_id"] if category_id is not None: try: - cat = discord.utils.get( - self.modmail_guild.categories, id=int(category_id) - ) + cat = discord.utils.get(self.modmail_guild.categories, id=int(category_id)) if cat is not None: return cat except ValueError: @@ -354,9 +348,7 @@ def command_perm(self, command_name: str) -> PermissionLevel: try: return PermissionLevel[level.upper()] except KeyError: - logger.warning( - "Invalid override_command_level for command %s.", command_name - ) + 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) @@ -405,11 +397,7 @@ async def setup_indexes(self): logger.info('Creating "text" index for logs collection.') logger.info("Name: %s", index_name) await coll.create_index( - [ - ("messages.content", "text"), - ("messages.author.name", "text"), - ("key", "text"), - ] + [("messages.content", "text"), ("messages.author.name", "text"), ("key", "text")] ) logger.debug("Successfully configured and verified database indexes.") @@ -428,8 +416,7 @@ async def on_ready(self): 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 + 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) @@ -447,9 +434,7 @@ async def on_ready(self): logger.line() for recipient_id, items in tuple(closures.items()): - after = ( - datetime.fromisoformat(items["time"]) - datetime.utcnow() - ).total_seconds() + after = (datetime.fromisoformat(items["time"]) - datetime.utcnow()).total_seconds() if after < 0: after = 0 @@ -475,9 +460,7 @@ 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"] - ) + logger.debug("Unable to resolve thread with channel %s.", log["channel_id"]) log_data = await self.api.post_log( log["channel_id"], { @@ -494,13 +477,10 @@ 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"], + "Failed to close thread with channel %s, skipping.", log["channel_id"] ) self.metadata_loop = tasks.Loop( @@ -550,9 +530,7 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: return sent_emoji, blocked_emoji - async def _process_blocked( - self, message: discord.Message - ) -> typing.Tuple[bool, str]: + 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: @@ -597,9 +575,7 @@ async def _process_blocked( logger.debug("Blocked due to account age, user %s.", message.author.name) if str(message.author.id) not in self.blocked_users: - new_reason = ( - f"System Message: New Account. Required to wait for {delta}." - ) + new_reason = f"System Message: New Account. Required to wait for {delta}." self.blocked_users[str(message.author.id)] = new_reason changed = True @@ -607,8 +583,7 @@ async def _process_blocked( await message.channel.send( embed=discord.Embed( title="Message not sent!", - description=f"Your must wait for {delta} " - f"before you can contact me.", + description=f"Your must wait for {delta} before you can contact me.", color=self.error_color, ) ) @@ -621,9 +596,7 @@ async def _process_blocked( logger.debug("Blocked due to guild age, user %s.", message.author.name) if str(message.author.id) not in self.blocked_users: - new_reason = ( - f"System Message: Recently Joined. Required to wait for {delta}." - ) + new_reason = f"System Message: Recently Joined. Required to wait for {delta}." self.blocked_users[str(message.author.id)] = new_reason changed = True @@ -631,8 +604,7 @@ async def _process_blocked( await message.channel.send( embed=discord.Embed( title="Message not sent!", - description=f"Your must wait for {delta} " - f"before you can contact me.", + description=f"Your must wait for {delta} before you can contact me.", color=self.error_color, ) ) @@ -643,9 +615,7 @@ async def _process_blocked( ): # Met the age limit already, otherwise it would've been caught by the previous if's reaction = sent_emoji - logger.debug( - "No longer internally blocked, user %s.", message.author.name - ) + logger.debug("No longer internally blocked, user %s.", message.author.name) self.blocked_users.pop(str(message.author.id)) else: reaction = blocked_emoji @@ -661,9 +631,7 @@ async def _process_blocked( ) if end_time is not None: - after = ( - datetime.fromisoformat(end_time.group(1)) - now - ).total_seconds() + after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() if after <= 0: # No longer blocked reaction = sent_emoji @@ -701,12 +669,10 @@ async def process_dm_modmail(self, message: discord.Message) -> None: description=self.config["disabled_new_thread_response"], ) embed.set_footer( - text=self.config["disabled_new_thread_footer"], - icon_url=self.guild.icon_url, + 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, + "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) @@ -724,8 +690,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: icon_url=self.guild.icon_url, ) logger.info( - "A message was blocked from %s due to disabled Modmail.", - message.author, + "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) @@ -866,9 +831,7 @@ async def process_commands(self, message): for ctx in ctxs: if ctx.command: if not any( - 1 - for check in ctx.command.checks - if hasattr(check, "permission_level") + 1 for check in ctx.command.checks if hasattr(check, "permission_level") ): logger.debug( "Command %s has no permissions check, adding invalid level.", @@ -1064,9 +1027,7 @@ async def on_command_error(self, context, exception): [c.__name__ for c in exception.converters] ) await context.trigger_typing() - await context.send( - embed=discord.Embed(color=self.error_color, description=msg) - ) + await context.send(embed=discord.Embed(color=self.error_color, description=msg)) elif isinstance(exception, commands.BadArgument): await context.trigger_typing() @@ -1082,9 +1043,7 @@ async def on_command_error(self, context, exception): if not await check(context): if hasattr(check, "fail_msg"): await context.send( - embed=discord.Embed( - color=self.error_color, description=check.fail_msg - ) + embed=discord.Embed(color=self.error_color, description=check.fail_msg) ) if hasattr(check, "permission_level"): corrected_permission_level = self.command_perm( diff --git a/cogs/modmail.py b/cogs/modmail.py index 8ce3f838fe..6a2b3bbfe8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -61,9 +61,7 @@ async def setup(self, ctx): return await ctx.send(embed=embed) overwrites = { - self.bot.modmail_guild.default_role: discord.PermissionOverwrite( - read_messages=False - ), + self.bot.modmail_guild.default_role: discord.PermissionOverwrite(read_messages=False), self.bot.modmail_guild.me: discord.PermissionOverwrite(read_messages=True), } @@ -108,9 +106,7 @@ async def setup(self, ctx): "feeling generous, check us out on [Patreon](https://patreon.com/kyber)!", ) - embed.set_footer( - text=f'Type "{self.bot.prefix}help" for a complete list of commands.' - ) + embed.set_footer(text=f'Type "{self.bot.prefix}help" for a complete list of commands.') await log_channel.send(embed=embed) self.bot.config["main_category_id"] = category.id @@ -126,10 +122,7 @@ async def setup(self, ctx): f"- `{self.bot.prefix}config help` for a list of available customizations." ) - if ( - not self.bot.config["command_permissions"] - and not self.bot.config["level_permissions"] - ): + if not self.bot.config["command_permissions"] and not self.bot.config["level_permissions"]: await self.bot.update_perms(PermissionLevel.REGULAR, -1) for owner_ids in self.bot.owner_ids: await self.bot.update_perms(PermissionLevel.OWNER, owner_ids) @@ -161,28 +154,21 @@ async def snippet(self, ctx, *, name: str.lower = None): if name is not None: val = self.bot.snippets.get(name) if val is None: - embed = create_not_found_embed( - name, self.bot.snippets.keys(), "Snippet" - ) + embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") return await ctx.send(embed=embed) return await ctx.send(escape_mentions(val)) if not self.bot.snippets: embed = discord.Embed( - color=self.bot.error_color, - description="You dont have any snippets at the moment.", - ) - embed.set_footer( - text=f"Do {self.bot.prefix}help snippet for more commands." + color=self.bot.error_color, description="You dont have any snippets at the moment." ) + embed.set_footer(text=f"Do {self.bot.prefix}help snippet for more commands.") embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) return await ctx.send(embed=embed) embeds = [] - for i, names in enumerate( - zip_longest(*(iter(sorted(self.bot.snippets)),) * 15) - ): + for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)): description = format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) @@ -233,7 +219,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte embed = discord.Embed( title="Error", color=self.bot.error_color, - description=f"Snippet names cannot be longer than 120 characters.", + description="Snippet names cannot be longer than 120 characters.", ) return await ctx.send(embed=embed) @@ -243,7 +229,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte embed = discord.Embed( title="Added snippet", color=self.bot.main_color, - description=f"Successfully created snippet.", + description="Successfully created snippet.", ) return await ctx.send(embed=embed) @@ -290,9 +276,7 @@ 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. @@ -336,9 +320,7 @@ async def send_scheduled_close_message(self, ctx, after, silent=False): if after.arg and not silent: embed.add_field(name="Message", value=after.arg) - embed.set_footer( - text="Closing will be cancelled " "if a thread message is sent." - ) + embed.set_footer(text="Closing will be cancelled if a thread message is sent.") embed.timestamp = after.dt await ctx.send(embed=embed) @@ -380,8 +362,7 @@ 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=self.bot.error_color, - description="Scheduled close has been cancelled.", + color=self.bot.error_color, description="Scheduled close has been cancelled." ) else: embed = discord.Embed( @@ -394,9 +375,7 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): if after and after.dt > now: await self.send_scheduled_close_message(ctx, after, silent) - await thread.close( - closer=ctx.author, after=close_after, message=message, silent=silent - ) + await thread.close(closer=ctx.author, after=close_after, message=message, silent=silent) @staticmethod def parse_user_or_role(ctx, user_or_role): @@ -445,8 +424,7 @@ async def notify( await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=f"{mention} will be mentioned " - "on the next message received.", + description=f"{mention} will be mentioned on the next message received.", ) return await ctx.send(embed=embed) @@ -483,8 +461,7 @@ async def unnotify( mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( - color=self.bot.main_color, - description=f"{mention} will no longer be notified.", + color=self.bot.main_color, description=f"{mention} will no longer be notified." ) return await ctx.send(embed=embed) @@ -517,15 +494,14 @@ async def subscribe( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=f"{mention} is already " "subscribed to this thread.", + description=f"{mention} is already subscribed to this thread.", ) else: mentions.append(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=f"{mention} will now be " - "notified of all messages received.", + description=f"{mention} will now be notified of all messages received.", ) return await ctx.send(embed=embed) @@ -556,14 +532,14 @@ async def unsubscribe( if mention not in mentions: embed = discord.Embed( color=self.bot.error_color, - description=f"{mention} is not already " "subscribed to this thread.", + description=f"{mention} is not already subscribed to this thread.", ) else: mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=f"{mention} is now unsubscribed " "to this thread.", + description=f"{mention} is now unsubscribed to this thread.", ) return await ctx.send(embed=embed) @@ -597,9 +573,7 @@ async def sfw(self, ctx): async def loglink(self, ctx): """Retrieves the link to the current thread's logs.""" log_link = await self.bot.api.get_log_link(ctx.channel.id) - await ctx.send( - embed=discord.Embed(color=self.bot.main_color, description=log_link) - ) + await ctx.send(embed=discord.Embed(color=self.bot.main_color, description=log_link)) def format_log_embeds(self, logs, avatar_url): embeds = [] @@ -618,13 +592,9 @@ def format_log_embeds(self, logs, avatar_url): username += entry["recipient"]["discriminator"] embed = discord.Embed(color=self.bot.main_color, timestamp=created_at) - embed.set_author( - name=f"{title} - {username}", icon_url=avatar_url, url=log_url - ) + embed.set_author(name=f"{title} - {username}", icon_url=avatar_url, url=log_url) embed.url = log_url - embed.add_field( - name="Created", value=duration(created_at, now=datetime.utcnow()) - ) + embed.add_field(name="Created", value=duration(created_at, now=datetime.utcnow())) closer = entry.get("closer") if closer is None: closer_msg = "Unknown" @@ -635,9 +605,7 @@ def format_log_embeds(self, logs, avatar_url): if entry["recipient"]["id"] != entry["creator"]["id"]: embed.add_field(name="Created by", value=f"<@{entry['creator']['id']}>") - embed.add_field( - name="Preview", value=format_preview(entry["messages"]), inline=False - ) + embed.add_field(name="Preview", value=format_preview(entry["messages"]), inline=False) if closer is not None: # BUG: Currently, logviewer can't display logs without a closer. @@ -677,7 +645,7 @@ async def logs(self, ctx, *, user: User = None): if not any(not log["open"] for log in logs): embed = discord.Embed( color=self.bot.error_color, - description="This user does not " "have any previous logs.", + description="This user does not have any previous logs.", ) return await ctx.send(embed=embed) @@ -699,11 +667,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): """ user = user if user is not None else ctx.author - query = { - "guild_id": str(self.bot.guild_id), - "open": False, - "closer.id": str(user.id), - } + query = {"guild_id": str(self.bot.guild_id), "open": False, "closer.id": str(user.id)} projection = {"messages": {"$slice": 5}} @@ -922,8 +886,7 @@ async def contact( if user.bot: embed = discord.Embed( - color=self.bot.error_color, - description="Cannot start a thread with a bot.", + color=self.bot.error_color, description="Cannot start a thread with a bot." ) return await ctx.send(embed=embed) @@ -937,16 +900,13 @@ async def contact( await ctx.channel.send(embed=embed) else: - thread = self.bot.threads.create( - user, creator=ctx.author, category=category - ) + 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} " - f"for {user.mention}.", + description=f"Thread started by {ctx.author.mention} for {user.mention}.", color=self.bot.main_color, ) await thread.wait_until_ready() @@ -965,11 +925,7 @@ async def contact( async def blocked(self, ctx): """Retrieve a list of blocked users.""" - embeds = [ - discord.Embed( - title="Blocked Users", color=self.bot.main_color, description="" - ) - ] + embeds = [discord.Embed(title="Blocked Users", color=self.bot.main_color, description="")] users = [] @@ -1062,9 +1018,7 @@ async def blocked_whitelist(self, ctx, *, user: User = None): @commands.command(usage="[user] [duration] [close message]") @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing - async def block( - self, ctx, user: Optional[User] = None, *, after: UserFriendlyTime = None - ): + async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTime = None): """ Block a user from using Modmail. @@ -1179,9 +1133,7 @@ async def unblock(self, ctx, *, user: User = None): ) else: embed = discord.Embed( - title="Error", - description=f"{mention} is not blocked.", - color=self.bot.error_color, + title="Error", description=f"{mention} is not blocked.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -1204,9 +1156,7 @@ async def delete(self, ctx, message_id: Optional[int] = None): try: message_id = int(message_id) except ValueError: - raise commands.BadArgument( - "An integer message ID needs to be specified." - ) + raise commands.BadArgument("An integer message ID needs to be specified.") linked_message_id = await self.find_linked_message(ctx, message_id) @@ -1236,7 +1186,7 @@ async def enable(self, ctx): """ embed = discord.Embed( title="Success", - description=f"Modmail will now accept all DM messages.", + description="Modmail will now accept all DM messages.", color=self.bot.main_color, ) @@ -1257,7 +1207,7 @@ async def disable(self, ctx): """ embed = discord.Embed( title="Success", - description=f"Modmail will not create any new threads.", + description="Modmail will not create any new threads.", color=self.bot.main_color, ) if self.bot.config["dm_disabled"] < 1: @@ -1276,7 +1226,7 @@ async def disable_all(self, ctx): """ embed = discord.Embed( title="Success", - description=f"Modmail will not accept any DM messages.", + description="Modmail will not accept any DM messages.", color=self.bot.main_color, ) @@ -1296,19 +1246,19 @@ async def isenable(self, ctx): if self.bot.config["dm_disabled"] == 1: embed = discord.Embed( title="New Threads Disabled", - description=f"Modmail is not creating new threads.", + description="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 is not accepting any DM messages for new and existing threads.", + description="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 accepting all DM messages.", + description="Modmail is accepting all DM messages.", color=self.bot.main_color, ) diff --git a/cogs/plugins.py b/cogs/plugins.py index 15db8214e4..8060799ce2 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -41,9 +41,7 @@ def __init__(self, user, repo, name, branch=None): @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): @@ -76,8 +74,7 @@ def from_string(cls, s, strict=False): 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) # pylint: disable=raising-format-tuple def __hash__(self): return hash((self.user, self.repo, self.name, self.branch)) @@ -129,14 +126,10 @@ 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: @@ -212,9 +205,7 @@ async def load_plugin(self, plugin): if stderr: logger.debug("[stderr]\n%s.", stderr.decode()) logger.error( - "Failed to download requirements for %s.", - plugin.ext_string, - exc_info=True, + "Failed to download requirements for %s.", plugin.ext_string, exc_info=True ) raise InvalidPluginError( f"Unable to download requirements: ```\n{stderr.decode()}\n```" @@ -250,9 +241,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): 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}`.", @@ -293,7 +282,8 @@ async def plugins_add(self, ctx, *, plugin_name: str): """ Install a new 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_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) @@ -302,8 +292,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=self.bot.error_color, + description="This plugin is already installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -324,10 +313,10 @@ async def plugins_add(self, ctx, *, plugin_name: str): try: await self.download_plugin(plugin, force=True) except Exception: - logger.warning(f"Unable to download plugin %s.", plugin, exc_info=True) + logger.warning("Unable to download plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", + description="Failed to download plugin, check logs for error.", color=self.bot.error_color, ) @@ -343,10 +332,10 @@ async def plugins_add(self, ctx, *, plugin_name: str): try: await self.load_plugin(plugin) except Exception: - logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) + logger.warning("Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", + description="Failed to download plugin, check logs for error.", color=self.bot.error_color, ) @@ -409,8 +398,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): pass # dir not empty embed = discord.Embed( - description="The plugin is successfully uninstalled.", - color=self.bot.main_color, + description="The plugin is successfully uninstalled.", color=self.bot.main_color ) await ctx.send(embed=embed) @@ -436,8 +424,7 @@ async def update_plugin(self, ctx, plugin_name): 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, + description=f"Successfully updated {plugin.name}.", color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -454,6 +441,7 @@ async def plugins_update(self, ctx, *, plugin_name: str = None): """ if plugin_name is None: + # pylint: disable=redefined-argument-from-local for plugin_name in self.bot.config["plugins"]: await self.update_plugin(ctx, plugin_name) else: @@ -483,8 +471,7 @@ async def plugins_loaded(self, ctx): if not self.loaded_plugins: embed = discord.Embed( - description="There are no plugins currently loaded.", - color=self.bot.error_color, + description="There are no plugins currently loaded.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -510,13 +497,9 @@ async def plugins_loaded(self, ctx): paginator = EmbedPaginatorSession(ctx, *embeds) await paginator.run() - @plugins.group( - invoke_without_command=True, name="registry", aliases=["list", "info"] - ) + @plugins.group(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. @@ -539,9 +522,7 @@ async def plugins_registry( if index >= len(registry): index = len(registry) - 1 else: - index = next( - (i for i, (n, _) in enumerate(registry) if plugin_name == n), 0 - ) + index = next((i for i, (n, _) in enumerate(registry) if plugin_name == n), 0) if not index and plugin_name is not None: embed = discord.Embed( @@ -553,8 +534,7 @@ async def plugins_registry( if matches: embed.add_field( - name="Perhaps you meant:", - value="\n".join(f"`{m}`" for m in matches), + name="Perhaps you meant:", value="\n".join(f"`{m}`" for m in matches) ) return await ctx.send(embed=embed) @@ -574,8 +554,7 @@ async def plugins_registry( ) 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( @@ -592,11 +571,9 @@ async def plugins_registry( 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, " + text="Your bot is unable to install this plugin, " f"minimum required version is v{required_version}." ) else: @@ -628,9 +605,7 @@ async def plugins_registry_compact(self, ctx): 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}" diff --git a/cogs/utility.py b/cogs/utility.py index b540483174..392656b650 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -67,11 +67,7 @@ async def format_cog_help(self, cog, *, no_cog=False): embed.add_field(name="Commands", value=format_ or "No commands.") continued = " (Continued)" if embeds else "" - name = ( - cog.qualified_name + " - Help" - if not no_cog - else "Miscellaneous Commands" - ) + name = cog.qualified_name + " - Help" if not no_cog else "Miscellaneous Commands" embed.set_author(name=name + continued, icon_url=bot.user.avatar_url) embed.set_footer( @@ -92,11 +88,7 @@ async def send_bot_help(self, mapping): bot = self.context.bot # always come first - default_cogs = [ - bot.get_cog("Modmail"), - bot.get_cog("Utility"), - bot.get_cog("Plugins"), - ] + default_cogs = [bot.get_cog("Modmail"), bot.get_cog("Utility"), bot.get_cog("Plugins")] default_cogs.extend(c for c in cogs if c not in default_cogs) @@ -105,16 +97,12 @@ async def send_bot_help(self, mapping): if no_cog_commands: embeds.extend(await self.format_cog_help(no_cog_commands, no_cog=True)) - session = EmbedPaginatorSession( - self.context, *embeds, destination=self.get_destination() - ) + session = EmbedPaginatorSession(self.context, *embeds, destination=self.get_destination()) return await session.run() async def send_cog_help(self, cog): embeds = await self.format_cog_help(cog) - session = EmbedPaginatorSession( - self.context, *embeds, destination=self.get_destination() - ) + session = EmbedPaginatorSession(self.context, *embeds, destination=self.get_destination()) return await session.run() async def _get_help_embed(self, topic): @@ -173,7 +161,7 @@ async def send_error_message(self, error): val = self.context.bot.snippets.get(command) if val is not None: return await self.get_destination().send( - escape_mentions(f"**`{command}` is a snippet, " f"content:**\n\n{val}") + escape_mentions(f"**`{command}` is a snippet, content:**\n\n{val}") ) val = self.context.bot.aliases.get(command) @@ -213,9 +201,7 @@ async def send_error_message(self, error): closest = get_close_matches(command, choices) if closest: - embed.add_field( - name=f"Perhaps you meant:", value="\n".join(f"`{x}`" for x in closest) - ) + embed.add_field(name="Perhaps you meant:", value="\n".join(f"`{x}`" for x in closest)) else: embed.title = "Cannot find command or category" embed.set_footer( @@ -239,7 +225,7 @@ def __init__(self, bot): }, ) self.bot.help_command.cog = self - self.loop_presence.start() + self.loop_presence.start() # pylint: disable=no-member def cog_unload(self): self.bot.help_command = self._original_help_command @@ -305,12 +291,11 @@ async def about(self, ctx): if self.bot.version.is_prerelease: stable = next( - filter( - lambda v: not parse_version(v.version).is_prerelease, - changelog.versions, - ) + 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}." ) - 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: @@ -365,8 +350,7 @@ async def debug(self, ctx): with open( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - f"../temp/{log_file_name}.log", + os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "r+", ) as f: @@ -421,17 +405,14 @@ async def debug_hastebin(self, ctx): with open( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - f"../temp/{log_file_name}.log", + os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "rb+", ) as f: logs = BytesIO(f.read().strip()) try: - async with self.bot.session.post( - haste_url + "/documents", data=logs - ) as resp: + async with self.bot.session.post(haste_url + "/documents", data=logs) as resp: data = await resp.json() try: key = data["key"] @@ -447,8 +428,7 @@ async def debug_hastebin(self, ctx): embed = discord.Embed( title="Debug Logs", color=self.bot.main_color, - description="Something's wrong. " - "We're unable to upload your logs to hastebin.", + description="Something's wrong. We're unable to upload your logs to hastebin.", ) embed.set_footer(text="Go to Heroku to see your logs.") await ctx.send(embed=embed) @@ -463,8 +443,7 @@ async def debug_clear(self, ctx): with open( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - f"../temp/{log_file_name}.log", + os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "w", ): @@ -527,9 +506,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): else: msg += f"{activity.name}." - embed = discord.Embed( - title="Activity Changed", description=msg, color=self.bot.main_color - ) + embed = discord.Embed(title="Activity Changed", description=msg, color=self.bot.main_color) return await ctx.send(embed=embed) @commands.command() @@ -566,14 +543,10 @@ async def status(self, ctx, *, status_type: str.lower): 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 - ) + 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=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") @@ -582,9 +555,7 @@ async def set_presence( 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".' @@ -600,9 +571,7 @@ async def set_presence( url = self.bot.config["twitch_url"] if activity_type is not None: - activity = discord.Activity( - type=activity_type, name=activity_message, url=url - ) + activity = discord.Activity(type=activity_type, name=activity_message, url=url) else: activity = None await self.bot.change_presence(activity=activity, status=status) @@ -664,9 +633,7 @@ async def mention(self, ctx, *, mention: str = None): if mention is None: embed = discord.Embed( - title="Current mention:", - color=self.bot.main_color, - description=str(current), + title="Current mention:", color=self.bot.main_color, description=str(current) ) else: embed = discord.Embed( @@ -761,9 +728,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): embed = exc.embed else: embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"{key} is an invalid key.", + title="Error", color=self.bot.error_color, description=f"{key} is an invalid key." ) valid_keys = [f"`{k}`" for k in sorted(keys)] embed.add_field(name="Valid keys", value=", ".join(valid_keys)) @@ -785,9 +750,7 @@ async def config_remove(self, ctx, *, key: str.lower): ) else: embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"{key} is an invalid key.", + title="Error", color=self.bot.error_color, description=f"{key} is an invalid key." ) valid_keys = [f"`{k}`" for k in sorted(keys)] embed.add_field(name="Valid keys", value=", ".join(valid_keys)) @@ -808,9 +771,7 @@ async def config_get(self, ctx, *, key: str.lower = None): if key in keys: desc = f"`{key}` is set to `{self.bot.config[key]}`" embed = discord.Embed(color=self.bot.main_color, description=desc) - embed.set_author( - name="Config variable", icon_url=self.bot.user.avatar_url - ) + embed.set_author(name="Config variable", icon_url=self.bot.user.avatar_url) else: embed = discord.Embed( @@ -825,12 +786,9 @@ async def config_get(self, ctx, *, key: str.lower = None): else: embed = discord.Embed( color=self.bot.main_color, - description="Here is a list of currently " - "set configuration variable(s).", - ) - embed.set_author( - name="Current config(s):", icon_url=self.bot.user.avatar_url + description="Here is a list of currently set configuration variable(s).", ) + embed.set_author(name="Current config(s):", icon_url=self.bot.user.avatar_url) config = self.bot.config.filter_default(self.bot.config) for name, value in config.items(): @@ -858,8 +816,7 @@ async def config_help(self, ctx, key: str.lower = None): ) if closest: embed.add_field( - name=f"Perhaps you meant:", - value="\n".join(f"`{x}`" for x in closest), + name=f"Perhaps you meant:", value="\n".join(f"`{x}`" for x in closest) ) return await ctx.send(embed=embed) @@ -882,13 +839,10 @@ def fmt(val): if current_key == key: index = i embed = discord.Embed( - title=f"Configuration description on {current_key}:", - color=self.bot.main_color, + title=f"Configuration description on {current_key}:", color=self.bot.main_color ) embed.add_field(name="Default:", value=fmt(info["default"]), inline=False) - embed.add_field( - name="Information:", value=fmt(info["description"]), inline=False - ) + embed.add_field(name="Information:", value=fmt(info["description"]), inline=False) if info["examples"]: example_text = "" for example in info["examples"]: @@ -937,9 +891,7 @@ async def alias(self, ctx, *, name: str.lower = None): if name is not None: val = self.bot.aliases.get(name) if val is None: - embed = utils.create_not_found_embed( - name, self.bot.aliases.keys(), "Alias" - ) + embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias") return await ctx.send(embed=embed) values = utils.parse_alias(val) @@ -949,7 +901,7 @@ async def alias(self, ctx, *, name: str.lower = None): title="Error", 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.", + "This alias will now be deleted.", ) self.bot.aliases.pop(name) await self.bot.config.update() @@ -972,8 +924,7 @@ async def alias(self, ctx, *, name: str.lower = None): if not self.bot.aliases: embed = discord.Embed( - color=self.bot.error_color, - description="You dont have any aliases at the moment.", + 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.") embed.set_author(name="Aliases", icon_url=ctx.guild.icon_url) @@ -1044,7 +995,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed = discord.Embed( title="Error", color=self.bot.error_color, - description=f"Alias names cannot be longer than 120 characters.", + description="Alias names cannot be longer than 120 characters.", ) if embed is not None: @@ -1264,9 +1215,7 @@ def _parse_level(name): @permissions.command(name="override") @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_override( - self, ctx, command_name: str.lower, *, level_name: str - ): + async def permissions_override(self, ctx, command_name: str.lower, *, level_name: str): """ Change a permission level for a specific command. @@ -1305,9 +1254,7 @@ async def permissions_override( command.qualified_name, level.name, ) - self.bot.config["override_command_level"][ - command.qualified_name - ] = level.name + self.bot.config["override_command_level"][command.qualified_name] = level.name await self.bot.config.update() embed = discord.Embed( @@ -1379,9 +1326,7 @@ async def permissions_add( key = self.bot.modmail_guild.get_member(value) if key is not None: logger.info("Granting %s access to Modmail category.", key.name) - await self.bot.main_category.set_permissions( - key, read_messages=True - ) + await self.bot.main_category.set_permissions(key, read_messages=True) embed = discord.Embed( title="Success", @@ -1478,21 +1423,13 @@ async def permissions_remove( self.bot.modmail_guild.default_role, read_messages=False ) elif isinstance(user_or_role, discord.Role): - logger.info( - "Denying %s access to Modmail category.", user_or_role.name - ) - await self.bot.main_category.set_permissions( - user_or_role, overwrite=None - ) + logger.info("Denying %s access to Modmail category.", user_or_role.name) + await self.bot.main_category.set_permissions(user_or_role, overwrite=None) else: member = self.bot.modmail_guild.get_member(value) if member is not None and member != self.bot.modmail_guild.me: - logger.info( - "Denying %s access to Modmail category.", member.name - ) - await self.bot.main_category.set_permissions( - member, overwrite=None - ) + logger.info("Denying %s access to Modmail category.", member.name) + await self.bot.main_category.set_permissions(member, overwrite=None) embed = discord.Embed( title="Success", @@ -1542,11 +1479,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[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. @@ -1596,9 +1529,7 @@ async def permissions_get( if value in permissions: levels.append(level.name) - mention = getattr( - user_or_role, "name", getattr(user_or_role, "id", user_or_role) - ) + mention = getattr(user_or_role, "name", getattr(user_or_role, "id", user_or_role)) desc_cmd = ( ", ".join(map(lambda x: f"`{x}`", cmds)) if cmds @@ -1648,14 +1579,10 @@ async def permissions_get( ) ) 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 = discord.Embed( color=self.bot.main_color, description=description @@ -1711,9 +1638,7 @@ async def permissions_get( return await ctx.send(embed=embed) if user_or_role == "command": - embeds.append( - self._get_perm(ctx, command.qualified_name, "command") - ) + embeds.append(self._get_perm(ctx, command.qualified_name, "command")) else: embeds.append(self._get_perm(ctx, level.name, "level")) else: @@ -1722,9 +1647,7 @@ async def permissions_get( for command in self.bot.walk_commands(): if command not in done: done.add(command) - embeds.append( - self._get_perm(ctx, command.qualified_name, "command") - ) + embeds.append(self._get_perm(ctx, command.qualified_name, "command")) else: for perm_level in PermissionLevel: embeds.append(self._get_perm(ctx, perm_level.name, "level")) @@ -1766,12 +1689,10 @@ async def oauth_whitelist(self, ctx, target: Union[discord.Role, utils.User]): embed.title = "Success" if not hasattr(target, "mention"): - target = self.bot.get_user(target.id) or self.bot.modmail_guild.get_role( - target.id - ) + target = self.bot.get_user(target.id) or self.bot.modmail_guild.get_role(target.id) embed.description = ( - f"{'Un-w' if removed else 'W'}hitelisted " f"{target.mention} to view logs." + f"{'Un-w' if removed else 'W'}hitelisted {target.mention} to view logs." ) await ctx.send(embed=embed) @@ -1796,12 +1717,8 @@ async def oauth_show(self, ctx): embed = discord.Embed(color=self.bot.main_color) embed.title = "Oauth Whitelist" - embed.add_field( - name="Users", value=" ".join(u.mention for u in users) or "None" - ) - embed.add_field( - name="Roles", value=" ".join(r.mention for r in roles) or "None" - ) + embed.add_field(name="Users", value=" ".join(u.mention for u in users) or "None") + embed.add_field(name="Roles", value=" ".join(r.mention for r in roles) or "None") await ctx.send(embed=embed) diff --git a/core/_color_data.py b/core/_color_data.py index ad98d3856f..0ac42d5c1f 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -43,9 +43,7 @@ } # 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 91856600e9..ace825482f 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -51,9 +51,7 @@ 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() @@ -91,9 +89,7 @@ def embed(self) -> Embed: """ 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, - url=self.url, + name=f"v{self.version} - Changelog", icon_url=self.bot.user.avatar_url, url=self.url ) for name, value in self.fields.items(): @@ -138,9 +134,7 @@ 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: @@ -174,10 +168,7 @@ async def from_url(cls, bot, url: str = "") -> "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" - ) + 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/checks.py b/core/checks.py index 0a5a43e1bb..7d387e6a5b 100644 --- a/core/checks.py +++ b/core/checks.py @@ -5,9 +5,7 @@ logger = getLogger(__name__) -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) diff --git a/core/clients.py b/core/clients.py index 8d89331664..7bc17943e5 100644 --- a/core/clients.py +++ b/core/clients.py @@ -66,9 +66,7 @@ async def request( `str` if the returned data is not a valid json data, the raw response. """ - async with self.session.request( - method, url, headers=headers, json=payload - ) as resp: + async with self.session.request(method, url, headers=headers, json=payload) as resp: if return_response: return resp try: @@ -120,7 +118,9 @@ async def get_log_link(self, channel_id: Union[str, int]) -> str: prefix = self.bot.config["log_url_prefix"].strip("/") if prefix == "NONE": prefix = "" - return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{doc['key']}" + return ( + f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{doc['key']}" + ) async def create_log_entry( self, recipient: Member, channel: TextChannel, creator: Member @@ -184,13 +184,9 @@ async def update_config(self, data: dict): {"bot_id": self.bot.user.id}, {"$set": toset, "$unset": unset} ) if toset: - return await self.db.config.update_one( - {"bot_id": self.bot.user.id}, {"$set": toset} - ) + return await self.db.config.update_one({"bot_id": self.bot.user.id}, {"$set": toset}) if unset: - return await self.db.config.update_one( - {"bot_id": self.bot.user.id}, {"$unset": unset} - ) + return await self.db.config.update_one({"bot_id": self.bot.user.id}, {"$unset": unset}) async def edit_message(self, message_id: Union[int, str], new_content: str) -> None: await self.logs.update_one( @@ -199,10 +195,7 @@ async def edit_message(self, message_id: Union[int, str], new_content: str) -> N ) async def append_log( - self, - message: Message, - channel_id: Union[str, int] = "", - type_: str = "thread_message", + self, message: Message, channel_id: Union[str, int] = "", type_: str = "thread_message" ) -> dict: channel_id = str(channel_id) or str(message.channel.id) data = { @@ -230,9 +223,7 @@ async def append_log( } return await self.logs.find_one_and_update( - {"channel_id": channel_id}, - {"$push": {f"messages": data}}, - return_document=True, + {"channel_id": channel_id}, {"$push": {"messages": data}}, return_document=True ) async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: diff --git a/core/config.py b/core/config.py index bfc4a3b675..ec8e66936d 100644 --- a/core/config.py +++ b/core/config.py @@ -146,9 +146,7 @@ def populate_cache(self) -> dict: data = deepcopy(self.defaults) # populate from env var and .env file - data.update( - {k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys} - ) + data.update({k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys}) config_json = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json" ) @@ -165,9 +163,7 @@ def populate_cache(self) -> dict: } ) except json.JSONDecodeError: - logger.critical( - "Failed to load config.json env values.", exc_info=True - ) + logger.critical("Failed to load config.json env values.", exc_info=True) self._cache = data config_help_json = os.path.join( @@ -229,7 +225,7 @@ def get(self, key: str, convert=True) -> typing.Any: elif key in self.time_deltas: if value is None: - return + return None try: value = isodate.parse_duration(value) except isodate.ISO8601Error: @@ -248,7 +244,7 @@ def get(self, key: str, convert=True) -> typing.Any: elif key in self.special_types: if value is None: - return + return None if key == "status": try: @@ -303,9 +299,7 @@ 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: @@ -343,9 +337,7 @@ def items(self) -> typing.Iterable: return self._cache.items() @classmethod - def filter_valid( - cls, data: typing.Dict[str, typing.Any] - ) -> typing.Dict[str, typing.Any]: + def filter_valid(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: return { k.lower(): v for k, v in data.items() @@ -353,9 +345,7 @@ def filter_valid( } @classmethod - def filter_default( - cls, data: typing.Dict[str, typing.Any] - ) -> typing.Dict[str, typing.Any]: + def filter_default(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: # TODO: use .get to prevent errors filtered = {} for k, v in data.items(): diff --git a/core/models.py b/core/models.py index 5a1b455f61..f55526f573 100644 --- a/core/models.py +++ b/core/models.py @@ -1,4 +1,3 @@ -import _string import logging import re import sys @@ -8,6 +7,8 @@ import discord from discord.ext import commands +import _string + try: from colorama import Fore, Style except ImportError: @@ -34,9 +35,7 @@ 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): @@ -82,10 +81,7 @@ def line(self, level="info"): if self.isEnabledFor(level): self._log( level, - Fore.BLACK - + Style.BRIGHT - + "-------------------------" - + Style.RESET_ALL, + Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL, [], ) @@ -97,8 +93,7 @@ def line(self, level="info"): 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", + "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", datefmt="%m/%d/%y %H:%M:%S" ) ch.setFormatter(formatter) diff --git a/core/paginator.py b/core/paginator.py index 835337dbed..695d194415 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -214,30 +214,28 @@ def __init__(self, ctx: commands.Context, *embeds, **options): footer_text = footer_text + " • " + embed.footer.text embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url) - def add_page(self, embed: Embed) -> None: - if isinstance(embed, Embed): - self.pages.append(embed) + def add_page(self, item: Embed) -> None: + if isinstance(item, Embed): + self.pages.append(item) else: raise TypeError("Page must be an Embed object.") - async def _create_base(self, embed: Embed) -> None: - self.base = await self.destination.send(embed=embed) + async def _create_base(self, item: Embed) -> None: + self.base = await self.destination.send(embed=item) async def _show_page(self, page): await self.base.edit(embed=page) class MessagePaginatorSession(PaginatorSession): - def __init__( - self, ctx: commands.Context, *messages, embed: Embed = None, **options - ): + def __init__(self, ctx: commands.Context, *messages, embed: Embed = None, **options): self.embed = embed self.footer_text = self.embed.footer.text if embed is not None else None super().__init__(ctx, *messages, **options) - def add_page(self, msg: str) -> None: - if isinstance(msg, str): - self.pages.append(msg) + def add_page(self, item: str) -> None: + if isinstance(item, str): + self.pages.append(item) else: raise TypeError("Page must be a str object.") @@ -248,9 +246,9 @@ def _set_footer(self): footer_text = footer_text + " • " + self.footer_text self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url) - async def _create_base(self, msg: str) -> None: + async def _create_base(self, item: str) -> None: self._set_footer() - self.base = await self.ctx.send(content=msg, embed=self.embed) + self.base = await self.ctx.send(content=item, embed=self.embed) async def _show_page(self, page) -> None: self._set_footer() diff --git a/core/thread.py b/core/thread.py index 03cefcae3d..9a40c07fd8 100644 --- a/core/thread.py +++ b/core/thread.py @@ -43,10 +43,7 @@ def __init__( self.auto_close_task = None def __repr__(self): - return ( - f'Thread(recipient="{self.recipient or self.id}", ' - f"channel={self.channel.id})" - ) + return f'Thread(recipient="{self.recipient or self.id}", ' f"channel={self.channel.id})" async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" @@ -85,9 +82,7 @@ async def setup(self, *, creator=None, category=None): # in case it creates a channel outside of category overwrites = { - self.bot.modmail_guild.default_role: discord.PermissionOverwrite( - read_messages=False - ) + self.bot.modmail_guild.default_role: discord.PermissionOverwrite(read_messages=False) } category = category or self.bot.main_category @@ -125,9 +120,7 @@ async def setup(self, *, creator=None, category=None): log_count = sum(1 for log in log_data if not log["open"]) except Exception: - logger.error( - "An error occurred while posting logs to the database.", exc_info=True - ) + logger.error("An error occurred while posting logs to the database.", exc_info=True) log_url = log_count = None # ensure core functionality still works @@ -211,9 +204,7 @@ def _format_info_embed(self, user, log_url, log_count, color): created = str((time - user.created_at).days) embed = discord.Embed( - color=color, - description=f"{user.mention} was created {days(created)}", - timestamp=time, + color=color, description=f"{user.mention} was created {days(created)}", timestamp=time ) # if not role_names: @@ -238,7 +229,7 @@ def _format_info_embed(self, user, log_url, log_count, color): embed.set_footer(text=f"{footer} • (not in main server)") if log_count is not None: - # embed.add_field(name='Past logs', value=f'{log_count}') + # embed.add_field(name="Past logs", value=f"{log_count}") thread = "thread" if log_count == 1 else "threads" embed.description += f" with **{log_count or 'no'}** past {thread}." else: @@ -365,7 +356,7 @@ async def _close( embed.title = user event = "Thread Closed as Scheduled" if scheduled else "Thread Closed" - # embed.set_author(name=f'Event: {event}', url=log_url) + # embed.set_author(name=f"Event: {event}", url=log_url) embed.set_footer(text=f"{event} by {_closer}") embed.timestamp = datetime.utcnow() @@ -389,10 +380,7 @@ 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 @@ -408,9 +396,7 @@ async def _close( await asyncio.gather(*tasks) async def cancel_closure( - self, - auto_close: bool = False, - all: bool = False, # pylint: disable=redefined-builtin + self, auto_close: bool = False, all: bool = False # pylint: disable=redefined-builtin ) -> None: if self.close_task is not None and (not auto_close or all): self.close_task.cancel() @@ -433,9 +419,7 @@ async def _find_thread_message(channel, message_id): if str(message_id) == str(embed.author.url).split("/")[-1]: return msg - async def _fetch_timeout( - self - ) -> typing.Union[None, isodate.duration.Duration, timedelta]: + async def _fetch_timeout(self) -> typing.Union[None, isodate.duration.Duration, timedelta]: """ This grabs the timeout value for closing threads automatically from the ConfigManager and parses it for use internally. @@ -481,10 +465,7 @@ async def _restart_close_timer(self): ) await self.close( - closer=self.bot.user, - after=int(seconds), - message=close_message, - auto_close=True, + closer=self.bot.user, after=int(seconds), message=close_message, auto_close=True ) async def edit_message(self, message_id: int, message: str) -> None: @@ -557,19 +538,12 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None else: # Send the same thing in the thread channel. tasks.append( - self.send( - message, - destination=self.channel, - from_mod=True, - anonymous=anonymous, - ) + self.send(message, destination=self.channel, from_mod=True, anonymous=anonymous) ) tasks.append( self.bot.api.append_log( - message, - self.channel.id, - type_="anonymous" if anonymous else "thread_message", + message, self.channel.id, type_="anonymous" if anonymous else "thread_message" ) ) @@ -626,16 +600,10 @@ async def send( embed = discord.Embed(description=message.content, timestamp=message.created_at) - system_avatar_url = ( - "https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png" - ) + system_avatar_url = "https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png" if not note: - if ( - anonymous - and from_mod - and not isinstance(destination, discord.TextChannel) - ): + if anonymous and from_mod and not isinstance(destination, discord.TextChannel): # Anonymously sending to the user. tag = self.bot.config["mod_tag"] if tag is None: @@ -656,9 +624,7 @@ async def send( 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}", + name=name, icon_url=avatar_url, url=f"https://discordapp.com/users/{author.id}" ) else: # Special note messages @@ -694,9 +660,7 @@ async def send( additional_count = 1 for url, filename in images: - if not prioritize_uploads or ( - is_image_url(url) and not embedded_image and filename - ): + if not prioritize_uploads or (is_image_url(url) and not embedded_image and filename): embed.set_image(url=url) if filename: embed.add_field(name="Image", value=f"[{filename}]({url})") @@ -713,9 +677,7 @@ async def send( img_embed.set_image(url=url) img_embed.title = filename img_embed.url = url - img_embed.set_footer( - text=f"Additional Image Upload ({additional_count})" - ) + img_embed.set_footer(text=f"Additional Image Upload ({additional_count})") img_embed.timestamp = message.created_at additional_images.append(destination.send(embed=img_embed)) additional_count += 1 @@ -755,14 +717,8 @@ async def send( except Exception as 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 - ) + 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() @@ -835,8 +791,7 @@ async def find( thread = self._find_from_channel(channel) if thread is None: user_id, thread = next( - ((k, v) for k, v in self.cache.items() if v.channel == channel), - (-1, None), + ((k, v) for k, v in self.cache.items() if v.channel == channel), (-1, None) ) if thread is not None: logger.debug("Found thread with tempered ID.") @@ -852,9 +807,7 @@ async def find( thread = self.cache[recipient_id] if not thread.channel or not self.bot.get_channel(thread.channel.id): self.bot.loop.create_task( - thread.close( - closer=self.bot.user, silent=True, delete_channel=False - ) + thread.close(closer=self.bot.user, silent=True, delete_channel=False) ) thread = None except KeyError: @@ -916,8 +869,7 @@ def format_channel_name(self, author): """Sanitises a username for use with text channel names""" name = author.name.lower() new_name = ( - "".join(l for l in name if l not in string.punctuation and l.isprintable()) - or "null" + "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" ) new_name += f"-{author.discriminator}" diff --git a/core/time.py b/core/time.py index ba27fa021c..f10892ec62 100644 --- a/core/time.py +++ b/core/time.py @@ -58,10 +58,7 @@ def __init__(self, argument): if not status.hasTime: # replace it with the current time dt = dt.replace( - hour=now.hour, - minute=now.minute, - second=now.second, - microsecond=now.microsecond, + hour=now.hour, minute=now.minute, second=now.second, microsecond=now.microsecond ) self.dt = dt diff --git a/core/utils.py b/core/utils.py index b62917119c..68c0870c71 100644 --- a/core/utils.py +++ b/core/utils.py @@ -20,7 +20,7 @@ def strtobool(val): val = val.lower() if val == "enable": return 1 - elif val == "disable": + if val == "disable": return 0 raise @@ -206,8 +206,7 @@ def match_user_id(text: str) -> int: 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.**", + color=discord.Color.red(), description=f"**{name.capitalize()} `{word}` cannot be found.**" ) val = get_close_matches(word, possibilities, n=n, cutoff=cutoff) if val: diff --git a/pyproject.toml b/pyproject.toml index 42d03a7617..c2d008bbc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,8 @@ [tool.black] -line-length = 88 -target-version = ['py37'] +line-length = 99 +target-version = ['py36'] include = '\.pyi?$' exclude = ''' - ( /( \.eggs From c6c723d6279b859e69dc14b9c802d226318504de Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 7 Nov 2019 02:44:27 -0800 Subject: [PATCH 12/38] Improved multicommand alias v3.3.1-dev1 --- CHANGELOG.md | 4 +- bot.py | 2 +- cogs/utility.py | 151 ++++++++++++++++++++---------------------------- core/utils.py | 40 +++++-------- 4 files changed, 81 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2cb7b1a6..b4b6268cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,18 @@ 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.1-dev0 +# v3.3.1-dev1 ### Added - "enable" and "disable" support for yes or no config vars. - Added "perhaps you meant" section to `?config help`. +- Multi-command alias is now more stable. With support for a single quote escape `\"`. ### Internal - Commit to black format line width max = 99, consistent with pylint. +- Alias parser is rewritten without shlex. # v3.3.0 diff --git a/bot.py b/bot.py index 48910db578..1589ae78ac 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.1-dev0" +__version__ = "3.3.1-dev1" import asyncio import logging diff --git a/cogs/utility.py b/cogs/utility.py index 392656b650..f87b0f552d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -16,6 +16,7 @@ import discord from discord.enums import ActivityType, Status from discord.ext import commands, tasks +from discord.ext.commands.view import StringView from discord.utils import escape_markdown, escape_mentions from aiohttp import ClientResponseError @@ -1002,6 +1003,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) values = utils.parse_alias(value) + save_aliases = [] if not values: embed = discord.Embed( @@ -1012,59 +1014,45 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') return await ctx.send(embed=embed) - if len(values) == 1: - linked_command, *messages = values[0].split(maxsplit=1) + multiple_alias = len(values) > 1 + + embed = discord.Embed( + title="Added alias", + color=self.bot.main_color + ) + + if multiple_alias: + embed.description = f'`{name}` points to: "{values[0]}".' + else: + embed.description = f"`{name}` now points to the following steps:" + + for i, val in enumerate(values, start=1): + view = StringView(val) + linked_command = view.get_word() + message = view.read_rest() + if not self.bot.get_command(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] + save_aliases.append(f"{alias_command} {message}".strip()) 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="Error", color=self.bot.error_color) - embed = discord.Embed( - title="Added alias", - color=self.bot.main_color, - description=f'`{name}` points to: "{values[0]}".', - ) + if multiple_alias: + embed.description = ("The command you are attempting to point " + f"to does not exist: `{linked_command}`.") + else: + embed.description = ("The command you are attempting to point " + f"to n step {i} does not exist: `{linked_command}`.") - else: - embed = discord.Embed( - title="Added alias", - color=self.bot.main_color, - description=f"`{name}` now points to the following steps:", - ) + return await ctx.send(embed=embed) + else: + save_aliases.append(val) - for i, val in enumerate(values, start=1): - linked_command, *messages = val.split(maxsplit=1) - if not self.bot.get_command(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 n step {i} does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) - embed.description += f"\n{i}: {val}" + embed.description += f"\n{i}: {val}" - self.bot.aliases[name] = " && ".join(values) + self.bot.aliases[name] = " && ".join(f"\"{a}\"" for a in save_aliases) await self.bot.config.update() - return await ctx.send(embed=embed) @alias.command(name="remove", aliases=["del", "delete"]) @@ -1097,6 +1085,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) values = utils.parse_alias(value) + save_aliases = [] if not values: embed = discord.Embed( @@ -1107,56 +1096,44 @@ async def alias_edit(self, ctx, name: str.lower, *, value): embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') return await ctx.send(embed=embed) - if len(values) == 1: - linked_command, *messages = values[0].split(maxsplit=1) + multiple_alias = len(values) > 1 + + embed = discord.Embed( + title="Edited alias", + color=self.bot.main_color + ) + + if multiple_alias: + embed.description = f'`{name}` points to: "{values[0]}".' + else: + embed.description = f"`{name}` now points to the following steps:" + + for i, val in enumerate(values, start=1): + view = StringView(val) + linked_command = view.get_word() + message = view.read_rest() + if not self.bot.get_command(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] + save_aliases.append(f"{alias_command} {message}".strip()) 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, - description=f'`{name}` now points to: "{values[0]}".', - ) + embed = discord.Embed(title="Error", color=self.bot.error_color) - else: - embed = discord.Embed( - title="Edited alias", - color=self.bot.main_color, - description=f"`{name}` now points to the following steps:", - ) - - for i, val in enumerate(values, start=1): - linked_command, *messages = val.split(maxsplit=1) - if not self.bot.get_command(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] + if multiple_alias: + embed.description = ("The command you are attempting to point " + f"to does not exist: `{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}" + embed.description = ("The command you are attempting to point " + f"to n step {i} does not exist: `{linked_command}`.") + + return await ctx.send(embed=embed) + else: + save_aliases.append(val) + + embed.description += f"\n{i}: {val}" - self.bot.aliases[name] = "&&".join(values) + self.bot.aliases[name] = " && ".join(f"\"{a}\"" for a in save_aliases) await self.bot.config.update() return await ctx.send(embed=embed) diff --git a/core/utils.py b/core/utils.py index 68c0870c71..23a89428b1 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,6 +1,6 @@ +import base64 import functools import re -import shlex import typing from difflib import get_close_matches from distutils.util import strtobool as _stb # pylint: disable=import-error @@ -215,35 +215,21 @@ def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discor def parse_alias(alias): - if "&&" not in alias: - if alias.startswith('"') and alias.endswith('"'): - return [alias[1:-1]] - return [alias] + def encode_alias(m): + return "\x1AU" + base64.b64encode(m.group(1).encode()).decode() + "\x1AU" - buffer = "" - cmd = [] - try: - for token in shlex.shlex(alias, punctuation_chars="&"): - if token != "&&": - buffer += " " + token - continue - - buffer = buffer.strip() - if buffer.startswith('"') and buffer.endswith('"'): - buffer = buffer[1:-1] - cmd += [buffer] - buffer = "" - except ValueError: - return [] + def decode_alias(m): + return base64.b64decode(m.group(1).encode()).decode() + + alias = re.sub(r"(?:(?<=^)(?:\s*(? Date: Thu, 7 Nov 2019 10:55:56 -0800 Subject: [PATCH 13/38] Fixed some issues with alias from last commit --- bot.py | 36 ++++++++++++----------------------- cogs/utility.py | 50 +++++++++++++++++++++++++------------------------ core/utils.py | 28 +++++++++++++++++++++++---- 3 files changed, 62 insertions(+), 52 deletions(-) diff --git a/bot.py b/bot.py index 1589ae78ac..cde9b247fa 100644 --- a/bot.py +++ b/bot.py @@ -34,7 +34,7 @@ from core import checks from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import human_join, parse_alias +from core.utils import human_join, parse_alias, normalize_alias from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging from core.thread import ThreadManager from core.time import human_timedelta @@ -723,32 +723,20 @@ async def get_contexts(self, message, *, cls=commands.Context): # Check if there is any aliases being called. alias = self.aliases.get(invoker) if alias is not None: - aliases = parse_alias(alias) + ctxs = [] + aliases = normalize_alias(alias, message.content[len(f"{invoked_prefix}{invoker}") :]) if not aliases: logger.warning("Alias %s is invalid, removing.", invoker) self.aliases.pop(invoker) - else: - len_ = len(f"{invoked_prefix}{invoker}") - contents = parse_alias(message.content[len_:]) - if not contents: - contents = [message.content[len_:]] - - ctxs = [] - for alias, content in zip_longest(aliases, contents): - if alias is None: - break - ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) - ctx.thread = await self.threads.find(channel=ctx.channel) - - if content is not None: - view = StringView(f"{alias} {content.strip()}") - else: - view = StringView(alias) - ctx.view = view - ctx.invoked_with = view.get_word() - ctx.command = self.all_commands.get(ctx.invoked_with) - ctxs += [ctx] - return ctxs + for alias in aliases: + view = StringView(alias) + ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) + ctx.thread = await self.threads.find(channel=ctx.channel) + ctx.view = view + ctx.invoked_with = view.get_word() + ctx.command = self.all_commands.get(ctx.invoked_with) + ctxs += [ctx] + return ctxs ctx.invoked_with = invoker ctx.command = self.all_commands.get(invoker) diff --git a/cogs/utility.py b/cogs/utility.py index f87b0f552d..84637ae427 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1016,34 +1016,35 @@ async def alias_add(self, ctx, name: str.lower, *, value): multiple_alias = len(values) > 1 - embed = discord.Embed( - title="Added alias", - color=self.bot.main_color - ) + embed = discord.Embed(title="Added alias", color=self.bot.main_color) - if multiple_alias: + if not multiple_alias: embed.description = f'`{name}` points to: "{values[0]}".' else: embed.description = f"`{name}` now points to the following steps:" for i, val in enumerate(values, start=1): view = StringView(val) - linked_command = view.get_word() + linked_command = view.get_word().lower() message = view.read_rest() if not self.bot.get_command(linked_command): alias_command = self.bot.aliases.get(linked_command) if alias_command is not None: - save_aliases.append(f"{alias_command} {message}".strip()) + save_aliases.extend(utils.normalize_alias(alias_command, message)) else: embed = discord.Embed(title="Error", color=self.bot.error_color) if multiple_alias: - embed.description = ("The command you are attempting to point " - f"to does not exist: `{linked_command}`.") + embed.description = ( + "The command you are attempting to point " + f"to does not exist: `{linked_command}`." + ) else: - embed.description = ("The command you are attempting to point " - f"to n step {i} does not exist: `{linked_command}`.") + embed.description = ( + "The command you are attempting to point " + f"to n step {i} does not exist: `{linked_command}`." + ) return await ctx.send(embed=embed) else: @@ -1051,7 +1052,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed.description += f"\n{i}: {val}" - self.bot.aliases[name] = " && ".join(f"\"{a}\"" for a in save_aliases) + self.bot.aliases[name] = " && ".join(f'"{a}"' for a in save_aliases) await self.bot.config.update() return await ctx.send(embed=embed) @@ -1098,34 +1099,35 @@ async def alias_edit(self, ctx, name: str.lower, *, value): multiple_alias = len(values) > 1 - embed = discord.Embed( - title="Edited alias", - color=self.bot.main_color - ) + embed = discord.Embed(title="Edited alias", color=self.bot.main_color) - if multiple_alias: + if not multiple_alias: embed.description = f'`{name}` points to: "{values[0]}".' else: embed.description = f"`{name}` now points to the following steps:" for i, val in enumerate(values, start=1): view = StringView(val) - linked_command = view.get_word() + linked_command = view.get_word().lower() message = view.read_rest() if not self.bot.get_command(linked_command): alias_command = self.bot.aliases.get(linked_command) if alias_command is not None: - save_aliases.append(f"{alias_command} {message}".strip()) + save_aliases.extend(utils.normalize_alias(alias_command, message)) else: embed = discord.Embed(title="Error", color=self.bot.error_color) if multiple_alias: - embed.description = ("The command you are attempting to point " - f"to does not exist: `{linked_command}`.") + embed.description = ( + "The command you are attempting to point " + f"to does not exist: `{linked_command}`." + ) else: - embed.description = ("The command you are attempting to point " - f"to n step {i} does not exist: `{linked_command}`.") + embed.description = ( + "The command you are attempting to point " + f"to n step {i} does not exist: `{linked_command}`." + ) return await ctx.send(embed=embed) else: @@ -1133,7 +1135,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value): embed.description += f"\n{i}: {val}" - self.bot.aliases[name] = " && ".join(f"\"{a}\"" for a in save_aliases) + self.bot.aliases[name] = " && ".join(f'"{a}"' for a in save_aliases) await self.bot.config.update() return await ctx.send(embed=embed) diff --git a/core/utils.py b/core/utils.py index 23a89428b1..44b890f230 100644 --- a/core/utils.py +++ b/core/utils.py @@ -4,7 +4,7 @@ import typing from difflib import get_close_matches from distutils.util import strtobool as _stb # pylint: disable=import-error -from itertools import takewhile +from itertools import takewhile, zip_longest from urllib import parse import discord @@ -221,9 +221,12 @@ def encode_alias(m): def decode_alias(m): return base64.b64decode(m.group(1).encode()).decode() - alias = re.sub(r"(?:(?<=^)(?:\s*(? Date: Thu, 7 Nov 2019 10:56:48 -0800 Subject: [PATCH 14/38] Unused imports --- bot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot.py b/bot.py index cde9b247fa..27c8e73e2b 100644 --- a/bot.py +++ b/bot.py @@ -7,7 +7,6 @@ import sys import typing from datetime import datetime -from itertools import zip_longest from types import SimpleNamespace import discord @@ -34,7 +33,7 @@ from core import checks from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import human_join, parse_alias, normalize_alias +from core.utils import human_join, normalize_alias from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging from core.thread import ThreadManager from core.time import human_timedelta From 0a7496103b2f96083b856d297bd407848d75ec51 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 12 Nov 2019 01:07:30 -0800 Subject: [PATCH 15/38] v3.3.1-dev2 --- CHANGELOG.md | 9 +- bot.py | 275 +++++++++++++++++++++++++----------------- core/clients.py | 7 ++ core/config.py | 39 +++--- core/config_help.json | 11 ++ core/thread.py | 22 ++-- core/time.py | 37 +++--- 7 files changed, 237 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4b6268cd0..4ff6ecaf37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,21 @@ 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.1-dev1 +# v3.3.1-dev2 ### Added +- Thread cooldown! + - Set via the new config var `thread_cooldown`. + - Specify a time for the recipient to wait before allowed to create another thread. - "enable" and "disable" support for yes or no config vars. - Added "perhaps you meant" section to `?config help`. - Multi-command alias is now more stable. With support for a single quote escape `\"`. +### Fixed + +- Setting config vars using human time wasn't working. + ### Internal - Commit to black format line width max = 99, consistent with pylint. diff --git a/bot.py b/bot.py index 27c8e73e2b..5e578cac7d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.1-dev1" +__version__ = "3.3.1-dev2" import asyncio import logging @@ -529,122 +529,171 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: return sent_emoji, blocked_emoji - 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: - if str(message.author.id) in self.blocked_users: - self.blocked_users.pop(str(message.author.id)) - await self.config.update() - - return False, sent_emoji - - now = datetime.utcnow() - + def check_account_age(self, author: discord.Member) -> bool: 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() - - reason = self.blocked_users.get(str(message.author.id)) or "" - min_guild_age = min_account_age = now + now = datetime.utcnow() try: - min_account_age = message.author.created_at + account_age + min_account_age = author.created_at + account_age except ValueError: logger.warning("Error with 'account_age'.", exc_info=True) - self.config.remove("account_age") - - try: - joined_at = getattr(message.author, "joined_at", None) - if joined_at is not None: - min_guild_age = joined_at + guild_age - except ValueError: - logger.warning("Error with 'guild_age'.", exc_info=True) - self.config.remove("guild_age") + min_account_age = author.created_at + self.config.remove("account_age") if min_account_age > now: # User account has not reached the required time - reaction = blocked_emoji - changed = False delta = human_timedelta(min_account_age) - logger.debug("Blocked due to account age, user %s.", message.author.name) + logger.debug("Blocked due to account age, user %s.", author.name) - if str(message.author.id) not in self.blocked_users: + if str(author.id) not in self.blocked_users: new_reason = f"System Message: New Account. Required to wait for {delta}." - self.blocked_users[str(message.author.id)] = new_reason - changed = True + self.blocked_users[str(author.id)] = new_reason - if reason.startswith("System Message: New Account.") or changed: - await message.channel.send( - embed=discord.Embed( - title="Message not sent!", - description=f"Your must wait for {delta} before you can contact me.", - color=self.error_color, - ) - ) + return False + return True + + def check_guild_age(self, author: discord.Member) -> bool: + guild_age = self.config.get("guild_age") + now = datetime.utcnow() + + if not hasattr(author, "joined_at"): + logger.warning("Not in guild, cannot verify guild_age, %s.", author.name) + return True - elif min_guild_age > now: + try: + min_guild_age = author.joined_at + guild_age + except ValueError: + logger.warning("Error with 'guild_age'.", exc_info=True) + min_guild_age = author.joined_at + self.config.remove("guild_age") + + if min_guild_age > now: # User has not stayed in the guild for long enough - reaction = blocked_emoji - changed = False delta = human_timedelta(min_guild_age) - logger.debug("Blocked due to guild age, user %s.", message.author.name) + logger.debug("Blocked due to guild age, user %s.", author.name) - if str(message.author.id) not in self.blocked_users: + if str(author.id) not in self.blocked_users: new_reason = f"System Message: Recently Joined. Required to wait for {delta}." - self.blocked_users[str(message.author.id)] = new_reason - changed = True + self.blocked_users[str(author.id)] = new_reason - if reason.startswith("System Message: Recently Joined.") or changed: - await message.channel.send( - embed=discord.Embed( - title="Message not sent!", - description=f"Your must wait for {delta} before you can contact me.", - color=self.error_color, - ) + return False + return True + + def check_manual_blocked(self, author: discord.Member) -> bool: + if str(author.id) not in self.blocked_users: + return True + + blocked_reason = self.blocked_users.get(str(author.id)) or "" + now = datetime.utcnow() + + if blocked_reason.startswith("System Message:"): + # Met the limits already, otherwise it would've been caught by the previous checks + logger.debug("No longer internally blocked, user %s.", author.name) + self.blocked_users.pop(str(author.id)) + return True + # etc "blah blah blah... until 2019-10-14T21:12:45.559948." + end_time = re.search(r"until ([^`]+?)\.$", blocked_reason) + if end_time is None: + # backwards compat + end_time = re.search(r"%([^%]+?)%", blocked_reason) + if end_time is not None: + logger.warning( + r"Deprecated time message for user %s, block and unblock again to update.", + author.name, ) - elif str(message.author.id) in self.blocked_users: - if reason.startswith("System Message: New Account.") or reason.startswith( - "System Message: Recently Joined." - ): - # Met the age limit already, otherwise it would've been caught by the previous if's - reaction = sent_emoji - logger.debug("No longer internally blocked, user %s.", message.author.name) - self.blocked_users.pop(str(message.author.id)) - else: - reaction = blocked_emoji - # 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: + after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() + if after <= 0: + # No longer blocked + self.blocked_users.pop(str(author.id)) + logger.debug("No longer blocked, user %s.", author.name) + return True + logger.debug("User blocked, user %s.", author.name) + return False + + async def _process_blocked(self, message): + sent_emoji, blocked_emoji = await self.retrieve_emoji() + if await self.is_blocked(message.author, channel=message.channel, send_message=True): + await self.add_reaction(message, blocked_emoji) + return True + return False + + async def is_blocked( + self, + author: discord.User, + *, + channel: discord.TextChannel = None, + send_message: bool = False, + ) -> typing.Tuple[bool, str]: + + member = self.guild.get_member(author.id) + if member is None: + logger.debug("User not in guild, %s.", author.id) + else: + author = member + + if str(author.id) in self.blocked_whitelisted_users: + if str(author.id) in self.blocked_users: + self.blocked_users.pop(str(author.id)) + await self.config.update() + return False + + blocked_reason = self.blocked_users.get(str(author.id)) or "" + + if ( + not self.check_account_age(author) + or not self.check_guild_age(author) + ): + new_reason = self.blocked_users.get(str(author.id)) + if new_reason != blocked_reason: + if send_message: + await channel.send( + embed=discord.Embed( + title="Message not sent!", + description=new_reason, + color=self.error_color, ) + ) + return True - if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() - if after <= 0: - # 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: - reaction = sent_emoji + if not self.check_manual_blocked(author): + return True await self.config.update() - return str(message.author.id) in self.blocked_users, reaction + return False + + async def get_thread_cooldown(self, author: discord.Member): + thread_cooldown = self.config.get("thread_cooldown") + now = datetime.utcnow() + + if thread_cooldown == isodate.Duration(): + return + + last_log = await self.api.get_latest_user_logs(author.id) + + if last_log is None: + logger.debug("Last thread wasn't found, %s.", author.name) + return + + last_log_closed_at = last_log.get("closed_at") + + if not last_log_closed_at: + logger.debug("Last thread was not closed, %s.", author.name) + return + + try: + cooldown = datetime.fromisoformat(last_log_closed_at) + thread_cooldown + except ValueError: + logger.warning("Error with 'thread_cooldown'.", exc_info=True) + cooldown = datetime.fromisoformat(last_log_closed_at) + self.config.remove( + "thread_cooldown" + ) + + if cooldown > now: + # User messaged before thread cooldown ended + delta = human_timedelta(cooldown) + logger.debug("Blocked due to thread cooldown, user %s.", author.name) + return delta + return @staticmethod async def add_reaction(msg, reaction): @@ -656,11 +705,24 @@ async def add_reaction(msg, reaction): async def process_dm_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" - blocked, reaction = await self._process_blocked(message) + blocked = await self._process_blocked(message) if blocked: - return await self.add_reaction(message, reaction) + return + sent_emoji, blocked_emoji = await self.retrieve_emoji() + thread = await self.threads.find(recipient=message.author) if thread is None: + delta = await self.get_thread_cooldown(message.author) + if delta: + await message.channel.send( + embed=discord.Embed( + title="Message not sent!", + description=f"You must wait for {delta} before you can contact me again.", + color=self.error_color, + ) + ) + return + if self.config["dm_disabled"] >= 1: embed = discord.Embed( title=self.config["disabled_new_thread_title"], @@ -673,9 +735,9 @@ async def process_dm_modmail(self, message: discord.Message) -> None: 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: @@ -691,12 +753,16 @@ async def process_dm_modmail(self, message: discord.Message) -> None: 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) + try: + await thread.send(message) + except Exception: + logger.error("Failed to send message:", exc_info=True) + await self.add_reaction(message, blocked_emoji) + else: + await self.add_reaction(message, sent_emoji) async def get_contexts(self, message, *, cls=commands.Context): """ @@ -849,9 +915,6 @@ async def on_typing(self, channel, user, _): if user.bot: return - async def _void(*_args, **_kwargs): - pass - if isinstance(channel, discord.DMChannel): if not self.config.get("user_typing"): return @@ -866,13 +929,7 @@ 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) - ) - ) - )[0]: + if await self.is_blocked(thread.recipient): return await thread.recipient.trigger_typing() diff --git a/core/clients.py b/core/clients.py index 7bc17943e5..d8814335bf 100644 --- a/core/clients.py +++ b/core/clients.py @@ -91,6 +91,13 @@ 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_latest_user_logs(self, user_id: Union[str, int]): + query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id), "open": False} + projection = {"messages": {"$slice": 5}} + logger.debug("Retrieving user %s latest logs.", user_id) + + return await self.logs.find_one(query, projection, limit=1, sort=[("closed_at", -1)]) + async def get_responded_logs(self, user_id: Union[str, int]) -> list: query = { "open": False, diff --git a/core/config.py b/core/config.py index ec8e66936d..695b743f56 100644 --- a/core/config.py +++ b/core/config.py @@ -13,7 +13,7 @@ from core._color_data import ALL_COLORS from core.models import InvalidConfigError, Default, getLogger -from core.time import UserFriendlyTime +from core.time import UserFriendlyTimeSync from core.utils import strtobool logger = getLogger(__name__) @@ -33,8 +33,9 @@ class ConfigManager: "error_color": str(discord.Color.red()), "user_typing": False, "mod_typing": False, - "account_age": None, - "guild_age": None, + "account_age": isodate.Duration(), + "guild_age": isodate.Duration(), + "thread_cooldown": isodate.Duration(), "reply_without_command": False, "anon_reply_without_command": False, # logging @@ -45,7 +46,7 @@ class ConfigManager: "close_emoji": "🔒", "recipient_thread_close": False, "thread_auto_close_silently": False, - "thread_auto_close": None, + "thread_auto_close": isodate.Duration(), "thread_auto_close_response": "This thread has been closed automatically due to inactivity after {timeout}.", "thread_creation_response": "The staff team will get back to you as soon as possible.", "thread_creation_footer": "Your message has been sent", @@ -115,7 +116,7 @@ class ConfigManager: colors = {"mod_color", "recipient_color", "main_color", "error_color"} - time_deltas = {"account_age", "guild_age", "thread_auto_close"} + time_deltas = {"account_age", "guild_age", "thread_auto_close", "thread_cooldown"} booleans = { "user_typing", @@ -224,17 +225,16 @@ def get(self, key: str, convert=True) -> typing.Any: value = int(self.remove(key).lstrip("#"), base=16) elif key in self.time_deltas: - if value is None: - return None - 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) + if not isinstance(value, isodate.Duration): + 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: @@ -298,13 +298,14 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: isodate.parse_duration(item) except isodate.ISO8601Error: try: - converter = UserFriendlyTime() - time = self.bot.loop.run_until_complete(converter.convert(None, item)) + converter = UserFriendlyTimeSync() + time = converter.convert(None, item) if time.arg: raise ValueError except BadArgument as exc: raise InvalidConfigError(*exc.args) - except Exception: + except Exception as e: + logger.debug(e) raise InvalidConfigError( "Unrecognized time, please use ISO-8601 duration format " 'string or a simpler "human readable" time.' diff --git a/core/config_help.json b/core/config_help.json index 0a6cee5076..c6c0fc95d0 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -222,6 +222,17 @@ "See also: `thread_auto_close_silently`, `thread_auto_close_response`." ] }, + "thread_cooldown": { + "default": "Never", + "description": "Specify the time required for the recipient to wait before allowed to create a new thread.", + "examples": [ + "`{prefix}config set thread_cooldown P12DT3H` (stands for 12 days and 3 hours in [ISO-8601 Duration Format](https://en.wikipedia.org/wiki/ISO_8601#Durations))", + "`{prefix}config set thread_cooldown 3 days and 5 hours` (accepted readable time)" + ], + "notes": [ + "To disable thread cooldown, do `{prefix}config del thread_cooldown`." + ] + }, "thread_auto_close_response": { "default": "\"This thread has been closed automatically due to inactivity after {{timeout}}.\"", "description": "This is the message to display when the thread when the thread auto-closes.", diff --git a/core/thread.py b/core/thread.py index 9a40c07fd8..bf13030776 100644 --- a/core/thread.py +++ b/core/thread.py @@ -295,8 +295,8 @@ async def _close( ): try: self.manager.cache.pop(self.id) - except KeyError: - logger.warning("Thread already closed.", exc_info=True) + except KeyError as e: + logger.error("Thread already closed: %s.", str(e)) return await self.cancel_closure(all=True) @@ -436,7 +436,7 @@ async def _restart_close_timer(self): timeout = await self._fetch_timeout() # Exit if timeout was not set - if not timeout: + if timeout == isodate.Duration(): return # Set timeout seconds @@ -723,8 +723,8 @@ async def send( try: await destination.trigger_typing() except discord.NotFound: - logger.warning("Channel not found.", exc_info=True) - return + logger.warning("Channel not found.") + raise if not from_mod and not note: mentions = self.get_notifications() @@ -804,12 +804,12 @@ async def find( recipient_id = recipient.id try: - thread = self.cache[recipient_id] - if not thread.channel or not self.bot.get_channel(thread.channel.id): - self.bot.loop.create_task( - thread.close(closer=self.bot.user, silent=True, delete_channel=False) - ) - thread = None + return self.cache[recipient_id] + # if not thread.channel or not self.bot.get_channel(thread.channel.id): + # self.bot.loop.create_task( + # thread.close(closer=self.bot.user, silent=True, delete_channel=False) + # ) + # thread = None except KeyError: channel = discord.utils.get( self.bot.modmail_guild.text_channels, topic=f"User ID: {recipient_id}" diff --git a/core/time.py b/core/time.py index f10892ec62..f91ad0dc09 100644 --- a/core/time.py +++ b/core/time.py @@ -84,32 +84,23 @@ def __init__(self, argument): raise BadArgument("The time is in the past.") -class UserFriendlyTime(Converter): +class UserFriendlyTimeSync(Converter): """That way quotes aren't absolutely necessary.""" - def __init__(self, converter: Converter = None): - if isinstance(converter, type) and issubclass(converter, Converter): - converter = converter() - - if converter is not None and not isinstance(converter, Converter): - raise TypeError("commands.Converter subclass necessary.") + def __init__(self): self.raw: str = None self.dt: datetime = None self.arg = None self.now: datetime = None - self.converter = converter - async def check_constraints(self, ctx, now, remaining): + def check_constraints(self, now, remaining): if self.dt < now: raise BadArgument("This time is in the past.") - if self.converter is not None: - self.arg = await self.converter.convert(ctx, remaining) - else: - self.arg = remaining + self.arg = remaining return self - async def convert(self, ctx, argument): + def convert(self, ctx, argument): self.raw = argument remaining = "" try: @@ -122,7 +113,7 @@ async def convert(self, ctx, argument): data = {k: int(v) for k, v in match.groupdict(default="0").items()} remaining = argument[match.end() :].strip() self.dt = self.now + relativedelta(**data) - return await self.check_constraints(ctx, self.now, remaining) + return self.check_constraints(self.now, remaining) # apparently nlp does not like "from now" # it likes "from x" in other cases though @@ -133,14 +124,9 @@ async def convert(self, ctx, argument): if argument.startswith("for "): argument = argument[4:].strip() - if argument[0:2] == "me": - # starts with "me to", "me in", or "me at " - if argument[0:6] in ("me to ", "me in ", "me at "): - argument = argument[6:] - elements = calendar.nlp(argument, sourceTime=self.now) if elements is None or not elements: - return await self.check_constraints(ctx, self.now, argument) + return self.check_constraints(self.now, argument) # handle the following cases: # "date time" foo @@ -151,7 +137,7 @@ async def convert(self, ctx, argument): dt, status, begin, end, _ = elements[0] if not status.hasDateOrTime: - return await self.check_constraints(ctx, self.now, argument) + return self.check_constraints(self.now, argument) if begin not in (0, 1) and end != len(argument): raise BadArgument( @@ -190,12 +176,17 @@ async def convert(self, ctx, argument): elif len(argument) == end: remaining = argument[:begin].strip() - return await self.check_constraints(ctx, self.now, remaining) + return self.check_constraints(self.now, remaining) except Exception: logger.exception("Something went wrong while parsing the time.") raise +class UserFriendlyTime(UserFriendlyTimeSync): + async def convert(self, ctx, argument): + return super().convert(ctx, argument) + + def human_timedelta(dt, *, source=None): now = source or datetime.utcnow() if dt > now: From cb00a0ff30c2b1ad9d3984937e6b63b326ba80ef Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 16 Nov 2019 00:55:12 -0800 Subject: [PATCH 16/38] Sync workflow and issue template from master --- .github/workflows/lints.yml | 4 ++-- bot.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 14a9447ab8..e5af38b6e0 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -26,7 +26,7 @@ jobs: run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - name: Pylint run: pylint ./bot.py cogs/*.py core/*.py --disable=import-error --exit-zero -r y - - name: Flake8 and black lint + - name: Black and flake8 run: | - flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 black . --check + flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 \ No newline at end of file diff --git a/bot.py b/bot.py index 5e578cac7d..3ad8d44180 100644 --- a/bot.py +++ b/bot.py @@ -639,10 +639,7 @@ async def is_blocked( blocked_reason = self.blocked_users.get(str(author.id)) or "" - if ( - not self.check_account_age(author) - or not self.check_guild_age(author) - ): + if not self.check_account_age(author) or not self.check_guild_age(author): new_reason = self.blocked_users.get(str(author.id)) if new_reason != blocked_reason: if send_message: From ca75885b60d23aec85019e8f053ad77ea5f28af7 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 16 Nov 2019 21:41:07 -0800 Subject: [PATCH 17/38] Improved dockerfile and changed stale limit --- .dockerignore | 3 +-- .github/workflows/lints.yml | 1 + .github/workflows/stale.yml | 5 +++-- Dockerfile | 11 ++++++++--- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.dockerignore b/.dockerignore index 57b6d8718c..742a36c7ae 100644 --- a/.dockerignore +++ b/.dockerignore @@ -145,8 +145,7 @@ app.json CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md -Pipfile -Pipfile.lock +requirements.min.txt Procfile pyproject.toml README.md diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index e5af38b6e0..3870c3f4da 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -26,6 +26,7 @@ jobs: run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - name: Pylint run: pylint ./bot.py cogs/*.py core/*.py --disable=import-error --exit-zero -r y + continue-on-error: true - name: Black and flake8 run: | black . --check diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8b02280dcb..75f607f8e2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,6 +10,7 @@ jobs: - uses: actions/stale@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'This issue is stale because it has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 5 days' - days-before-stale: 90 + stale-issue-message: 'This issue is stale because it has been open for 100 days with no activity. Remove stale label or comment or this will be closed in 5 days. Please do not un-stale this issue unless it carries significant contribution.' + days-before-stale: 100 days-before-close: 5 + exempt-issue-label: 'high priority' diff --git a/Dockerfile b/Dockerfile index eadf05e4e2..f616ef29f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,10 @@ -FROM python:3.7.4-alpine +FROM python:3.7-alpine WORKDIR /modmailbot COPY . /modmailbot -RUN pip install --no-cache-dir -r requirements.min.txt -CMD ["python", "bot.py"] \ No newline at end of file +RUN export PIP_NO_CACHE_DIR=false \ + && apk update \ + && apk add --update --no-cache --virtual .build-deps alpine-sdk \ + && pip install pipenv \ + && pipenv install --deploy --ignore-pipfile \ + && apk del .build-deps +CMD ["pipenv", "run", "bot"] \ No newline at end of file From 48e801d688ff433199e8aa8b8553a3f0b2488d58 Mon Sep 17 00:00:00 2001 From: DAzVise Date: Tue, 19 Nov 2019 18:31:26 +0300 Subject: [PATCH 18/38] better logic --- cogs/modmail.py | 2 +- core/thread.py | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 6a2b3bbfe8..cee11f84bc 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -900,7 +900,7 @@ async def contact( await ctx.channel.send(embed=embed) else: - thread = self.bot.threads.create(user, creator=ctx.author, category=category) + thread = await 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) diff --git a/core/thread.py b/core/thread.py index 0e6d43e12a..83f7f5267b 100644 --- a/core/thread.py +++ b/core/thread.py @@ -97,7 +97,7 @@ async def setup(self, *, creator=None, category=None): overwrites=overwrites, reason="Creating a thread channel.", ) - except discord.HTTPException as e: # Failed to create due to 50 channel limit. + except discord.HTTPException as e: # Failed to create due to missing perms. logger.critical("An error occurred while creating a thread.", exc_info=True) self.manager.cache.pop(self.id) @@ -860,18 +860,17 @@ async def create( # Schedule thread setup for later cat = self.bot.main_category - if len(cat.channels) == 50: + if category is None and len(cat.channels) == 50: fallback_id = self.bot.config["fallback_category_id"] - fallback = discord.utils.get(cat.guild.categories, id=int(fallback_id)) - if fallback and len(fallback.channels) != 50: - self.bot.loop.create_task(thread.setup(creator=creator, category=fallback)) - return thread - - fallback = await cat.clone(name="Fallback Modmail") - self.bot.config.set("fallback_category_id", fallback.id) - await self.bot.config.update() - self.bot.loop.create_task(thread.setup(creator=creator, category=fallback)) - return thread + if fallback_id: + fallback = discord.utils.get(cat.guild.categories, id=int(fallback_id)) + if fallback and len(fallback.channels) != 50: + category = fallback + + if not category: + category = await cat.clone(name="Fallback Modmail") + self.bot.config.set("fallback_category_id", category.id) + await self.bot.config.update() self.bot.loop.create_task(thread.setup(creator=creator, category=category)) return thread From 9e81e23b004f72cd8dadcc0c758055c4a39efce2 Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sun, 24 Nov 2019 08:42:02 -0500 Subject: [PATCH 19/38] Change block help to fix mistake. --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 6a2b3bbfe8..fb3361cb32 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1015,7 +1015,7 @@ async def blocked_whitelist(self, ctx, *, user: User = None): return await ctx.send(embed=embed) - @commands.command(usage="[user] [duration] [close message]") + @commands.command(usage="[user] [duration] [reason]") @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTime = None): From d7c511d902543394de754ee6dd5a808d990ec73b Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 26 Nov 2019 12:52:07 -0800 Subject: [PATCH 20/38] 3.3.2-dev3 fallback category --- .github/workflows/lints.yml | 13 ++++++++++--- CHANGELOG.md | 5 ++++- bot.py | 2 +- core/config_help.json | 7 +++++-- requirements.min.txt | 2 +- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 3870c3f4da..852b3e1e07 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -1,14 +1,21 @@ name: Modmail Workflow -on: [push, pull_request, pull_request_review] +on: [push, pull_request] jobs: code-style: - runs-on: ${{ matrix.os }} + +# runs-on: ${{ matrix.os }} +# strategy: +# fail-fast: false +# matrix: +# os: [ubuntu-latest, windows-latest, macOS-latest] +# python-version: [3.6, 3.7] + + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] python-version: [3.6, 3.7] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index d19a0ec37f..0d6d81066b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,16 @@ 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.2-dev2 +# v3.3.2-dev3 ### Added - Thread cooldown! - Set via the new config var `thread_cooldown`. - Specify a time for the recipient to wait before allowed to create another thread. +- Fallback Category (thanks to DAzVise PR#636) + - Automatically created upon reaching the 50 channels limit. + - Manually set fallback category with the config var `fallback_category_id`. - "enable" and "disable" support for yes or no config vars. - Added "perhaps you meant" section to `?config help`. - Multi-command alias is now more stable. With support for a single quote escape `\"`. diff --git a/bot.py b/bot.py index 5276f2e62f..44d9d002e8 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.2-dev2" +__version__ = "3.3.2-dev3" import asyncio diff --git a/core/config_help.json b/core/config_help.json index 177e488094..db9218d2b3 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -17,7 +17,9 @@ "`{prefix}config set main_category_id 9234932582312` (`9234932582312` is the category ID)" ], "notes": [ - "If the Modmail category ended up being non-existent/invalid, Modmail will break. To fix this, run `{prefix}setup` again or set `main_category_id` to a valid category." + "If the Modmail category ended up being non-existent/invalid, Modmail will break. To fix this, run `{prefix}setup` again or set `main_category_id` to a valid category.", + "When the Modmail category is full, new channels will be created in the fallback category.", + "See also: `fallback_category_id`." ] }, "fallback_category_id": { @@ -27,7 +29,8 @@ "`{prefix}config set fallback_category_id 9234932582312` (`9234932582312` is the category ID)" ], "notes": [ - "If the Fallback category ended up being non-existent/invalid, Modmail will create a new one. To fix this, set `fallback_category_id` to a valid category." + "If the Fallback category ended up being non-existent/invalid, Modmail will create a new one. To fix this, set `fallback_category_id` to a valid category.", + "See also: `main_category_id`." ] }, "prefix": { diff --git a/requirements.min.txt b/requirements.min.txt index c8c5c2ab07..dded194188 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -6,7 +6,7 @@ aiohttp==3.5.4 async-timeout==3.0.1 attrs==19.3.0 chardet==3.0.4 -discord.py==1.2.4 +discord.py==1.2.5 dnspython==1.16.0 emoji==0.5.4 future==0.18.1 From 013a1cff72abde072012b59465721533ed30dcdf Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 27 Nov 2019 15:35:18 -0800 Subject: [PATCH 21/38] Fix typo --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 83f7f5267b..79651491c5 100644 --- a/core/thread.py +++ b/core/thread.py @@ -43,7 +43,7 @@ def __init__( self.auto_close_task = None def __repr__(self): - return f'Thread(recipient="{self.recipient or self.id}", ' f"channel={self.channel.id})" + return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id})' async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" From faeaf49c07041d596ca3b0781857627100958434 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 28 Nov 2019 00:15:29 -0800 Subject: [PATCH 22/38] Fixed typos --- .dockerignore | 1 + .gitignore | 1 + cogs/modmail.py | 46 +++++++++++++++++++++++----------------------- cogs/utility.py | 30 +++++++++++++++--------------- core/time.py | 6 +++--- pyproject.toml | 1 + 6 files changed, 44 insertions(+), 41 deletions(-) diff --git a/.dockerignore b/.dockerignore index 742a36c7ae..bae84fb2e4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -93,6 +93,7 @@ celerybeat-schedule .venv env/ venv/ +venv2/ ENV/ env.bak/ venv.bak/ diff --git a/.gitignore b/.gitignore index a7fb960a6b..79f95c8388 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,7 @@ celerybeat-schedule .venv env/ venv/ +venv2/ ENV/ env.bak/ venv.bak/ diff --git a/cogs/modmail.py b/cogs/modmail.py index 83c3cb0886..5488f71c71 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -100,10 +100,10 @@ async def setup(self, ctx): ) embed.add_field( - name="Thanks for using the bot!", + name="Thanks for using our bot!", value="If you like what you see, consider giving the " - "[repo a star](https://github.com/kyb3r/modmail) :star: or if you are " - "feeling generous, check us out on [Patreon](https://patreon.com/kyber)!", + "[repo a star](https://github.com/kyb3r/modmail) :star: and if you are " + "feeling extra generous, buy us coffee on [Patreon](https://patreon.com/kyber) :heart:!", ) embed.set_footer(text=f'Type "{self.bot.prefix}help" for a complete list of commands.') @@ -115,8 +115,8 @@ async def setup(self, ctx): await self.bot.config.update() await ctx.send( "**Successfully set up server.**\n" - "Consider setting permission levels " - "to give access to roles or users the ability to use Modmail.\n\n" + "Consider setting permission levels to give access to roles " + "or users the ability to use Modmail.\n\n" f"Type:\n- `{self.bot.prefix}permissions` and `{self.bot.prefix}permissions add` " "for more info on setting permissions.\n" f"- `{self.bot.prefix}config help` for a list of available customizations." @@ -162,7 +162,7 @@ async def snippet(self, ctx, *, name: str.lower = None): embed = discord.Embed( color=self.bot.error_color, description="You dont have any snippets at the moment." ) - embed.set_footer(text=f"Do {self.bot.prefix}help snippet for more commands.") + embed.set_footer(text=f'Check "{self.bot.prefix}help snippet" to add a snippet.') embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) return await ctx.send(embed=embed) @@ -211,7 +211,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte embed = discord.Embed( title="Error", color=self.bot.error_color, - description=f"An alias with the same name already exists: `{name}`.", + description=f"An alias that shares the same name exists: `{name}`.", ) return await ctx.send(embed=embed) @@ -397,7 +397,7 @@ async def notify( """ Notify a user or role when the next thread message received. - Once a thread message is received, `user_or_role` will only be pinged once. + Once a thread message is received, `user_or_role` will be pinged once. Leave `user_or_role` empty to notify yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. @@ -405,7 +405,7 @@ async def notify( """ 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.") + raise commands.BadArgument(f"{user_or_role} is not a valid user or role.") thread = ctx.thread @@ -482,7 +482,7 @@ async def subscribe( """ 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.") + raise commands.BadArgument(f"{user_or_role} is not a valid user or role.") thread = ctx.thread @@ -539,7 +539,7 @@ async def unsubscribe( await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=f"{mention} is now unsubscribed to this thread.", + description=f"{mention} is now unsubscribed from this thread.", ) return await ctx.send(embed=embed) @@ -649,7 +649,7 @@ async def logs(self, ctx, *, user: User = None): ) return await ctx.send(embed=embed) - logs = reversed([e for e in logs if not e["open"]]) + logs = reversed([log for log in logs if not log["open"]]) embeds = self.format_log_embeds(logs, avatar_url=icon_url) @@ -678,7 +678,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( color=self.bot.error_color, - description="No log entries have been found for that query", + description="No log entries have been found for that query.", ) return await ctx.send(embed=embed) @@ -838,8 +838,8 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): """ Edit a message that was sent using the reply or anonreply command. - If no `message_id` is provided, the - last message sent by a staff will be edited. + If no `message_id` is provided, + the last message sent by a staff will be edited. """ thread = ctx.thread @@ -1037,7 +1037,7 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi elif after is None: raise commands.MissingRequiredArgument(SimpleNamespace(name="user")) else: - raise commands.BadArgument(f'User "{after.arg}" not found') + raise commands.BadArgument(f'User "{after.arg}" not found.') mention = getattr(user, "mention", f"`{user.id}`") @@ -1069,8 +1069,8 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi 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}", + description=f"{mention} was previously blocked {old_reason}.\n" + f"{mention} is now blocked {reason}", color=self.bot.main_color, ) else: @@ -1116,14 +1116,14 @@ 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 " - f"{reason}.\n{mention} is no longer blocked.", + description=f"{mention} was previously blocked internally {reason}.\n" + f"{mention} is no longer blocked.", color=self.bot.main_color, ) embed.set_footer( 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.' + f"{name} will be automatically blocked again. " + f'Use "{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' ) else: embed = discord.Embed( @@ -1156,7 +1156,7 @@ async def delete(self, ctx, message_id: Optional[int] = None): try: message_id = int(message_id) except ValueError: - raise commands.BadArgument("An integer message ID needs to be specified.") + raise commands.BadArgument("A message ID needs to be specified.") linked_message_id = await self.find_linked_message(ctx, message_id) diff --git a/cogs/utility.py b/cogs/utility.py index 84637ae427..4f44856a8e 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -184,8 +184,8 @@ async def send_error_message(self, error): for i, val in enumerate(values, start=1): embed.description += f"\n{i}: {escape_markdown(val)}" embed.set_footer( - text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" for more ' - "details on aliases." + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" ' + "for more details on aliases." ) return await self.get_destination().send(embed=embed) @@ -237,7 +237,7 @@ def cog_unload(self): async def changelog(self, ctx, version: str.lower = ""): """Shows the changelog of the Modmail.""" changelog = await Changelog.from_url(self.bot) - version = version.lstrip("vV") if version else changelog.latest_version.version + version = version.lstrip("v") if version else changelog.latest_version.version try: index = [v.version for v in changelog.versions].index(version) @@ -264,7 +264,7 @@ async def changelog(self, ctx, version: str.lower = ""): f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" ) - @commands.command(aliases=["bot", "info"]) + @commands.command(aliases=["info"]) @checks.has_permissions(PermissionLevel.REGULAR) @utils.trigger_typing async def about(self, ctx): @@ -345,7 +345,7 @@ async def sponsors(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) @utils.trigger_typing async def debug(self, ctx): - """Shows the recent application-logs of the bot.""" + """Shows the recent application logs of the bot.""" log_file_name = self.bot.token.split(".")[0] @@ -368,23 +368,23 @@ async def debug(self, ctx): messages = [] - # Using Scala formatting because it's similar to Python for exceptions + # Using Haskell formatting because it's similar to Python for exceptions # and it does a fine job formatting the logs. - msg = "```Scala\n" + msg = "```Haskell\n" for line in logs.splitlines(keepends=True): - if msg != "```Scala\n": + if msg != "```Haskell\n": if len(line) + len(msg) + 3 > 2000: msg += "```" messages.append(msg) - msg = "```Scala\n" + msg = "```Haskell\n" msg += line if len(msg) + 3 > 2000: msg = msg[:1993] + "[...]```" messages.append(msg) - msg = "```Scala\n" + msg = "```Haskell\n" - if msg != "```Scala\n": + if msg != "```Haskell\n": msg += "```" messages.append(msg) @@ -1043,7 +1043,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): else: embed.description = ( "The command you are attempting to point " - f"to n step {i} does not exist: `{linked_command}`." + f"to in step {i} does not exist: `{linked_command}`." ) return await ctx.send(embed=embed) @@ -1266,7 +1266,7 @@ async def permissions_add( - `{prefix}perms add command "plugin enabled" @role` - `{prefix}perms add command help 984301093849028` - Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, + Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead. """ if type_ not in {"command", "level"}: @@ -1342,7 +1342,7 @@ async def permissions_remove( - `{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, + Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead. """ if type_ not in {"command", "level", "override"} or ( type_ != "override" and user_or_role is None @@ -1484,7 +1484,7 @@ async def permissions_get( - `{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, + 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", "override"}: diff --git a/core/time.py b/core/time.py index f91ad0dc09..331e26349f 100644 --- a/core/time.py +++ b/core/time.py @@ -120,9 +120,9 @@ def convert(self, ctx, argument): # so let me handle the 'now' case if argument.endswith(" from now"): argument = argument[:-9].strip() - # handles "for xxx hours" - if argument.startswith("for "): - argument = argument[4:].strip() + # handles "in xxx hours" + if argument.startswith("in "): + argument = argument[3:].strip() elements = calendar.nlp(argument, sourceTime=self.now) if elements is None or not elements: diff --git a/pyproject.toml b/pyproject.toml index c2d008bbc8..420b418e6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ exclude = ''' | \.git | \.venv | venv + | venv2 | _build | build | dist From e9e89925d5e28aed85284778d8e473d355e69be5 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 29 Nov 2019 22:46:24 -0800 Subject: [PATCH 23/38] v3.3.2-dev4 freply, fixed alias bugs, alias and snippet looks --- CHANGELOG.md | 8 +- bot.py | 27 +++--- cogs/modmail.py | 45 ++++++++-- cogs/plugins.py | 8 +- cogs/utility.py | 222 ++++++++++++++++++++---------------------------- core/clients.py | 4 +- core/utils.py | 24 ++++-- 7 files changed, 172 insertions(+), 166 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6d81066b..54389ce6f4 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.2-dev3 +# v3.3.2-dev4 ### Added @@ -20,10 +20,16 @@ however, insignificant breaking changes does not guarantee a major version bump, - "enable" and "disable" support for yes or no config vars. - Added "perhaps you meant" section to `?config help`. - Multi-command alias is now more stable. With support for a single quote escape `\"`. +- New command `?freply`, which behaves exactly like `?reply` with the addition that you can substitute `{channel}`, `{recipient}`, and `{author}` to be their respective values. + +### Changed + +- The look of alias and snippet when previewing. ### Fixed - Setting config vars using human time wasn't working. +- Fixed some bugs with aliases. ### Internal diff --git a/bot.py b/bot.py index 44d9d002e8..5a9ea676e3 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.2-dev3" +__version__ = "3.3.2-dev4" import asyncio @@ -612,7 +612,7 @@ def check_manual_blocked(self, author: discord.Member) -> bool: return False async def _process_blocked(self, message): - sent_emoji, blocked_emoji = await self.retrieve_emoji() + _, blocked_emoji = await self.retrieve_emoji() if await self.is_blocked(message.author, channel=message.channel, send_message=True): await self.add_reaction(message, blocked_emoji) return True @@ -770,7 +770,7 @@ async def get_contexts(self, message, *, cls=commands.Context): view = StringView(message.content) ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) - ctx.thread = await self.threads.find(channel=ctx.channel) + thread = await self.threads.find(channel=ctx.channel) if self._skip_check(message.author.id, self.user.id): return [ctx] @@ -791,16 +791,18 @@ async def get_contexts(self, message, *, cls=commands.Context): if not aliases: logger.warning("Alias %s is invalid, removing.", invoker) self.aliases.pop(invoker) + for alias in aliases: - view = StringView(alias) - ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) - ctx.thread = await self.threads.find(channel=ctx.channel) - ctx.view = view - ctx.invoked_with = view.get_word() - ctx.command = self.all_commands.get(ctx.invoked_with) - ctxs += [ctx] + view = StringView(invoked_prefix + alias) + ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message) + ctx_.thread = thread + discord.utils.find(view.skip_string, prefixes) + ctx_.invoked_with = view.get_word().lower() + ctx_.command = self.all_commands.get(ctx_.invoked_with) + ctxs += [ctx_] return ctxs + ctx.thread = thread ctx.invoked_with = invoker ctx.command = self.all_commands.get(invoker) return [ctx] @@ -872,11 +874,8 @@ async def process_commands(self, message): # Process snippets if cmd in self.snippets: - thread = await self.threads.find(channel=message.channel) snippet = self.snippets[cmd] - if thread: - snippet = self.formatter.format(snippet, recipient=thread.recipient) - message.content = f"{self.prefix}reply {snippet}" + message.content = f"{self.prefix}freply {snippet}" ctxs = await self.get_contexts(message) for ctx in ctxs: diff --git a/cogs/modmail.py b/cogs/modmail.py index 5488f71c71..29f4132149 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -6,7 +6,7 @@ import discord from discord.ext import commands -from discord.utils import escape_markdown, escape_mentions +from discord.utils import escape_markdown from dateutil import parser from natural.date import duration @@ -21,6 +21,7 @@ create_not_found_embed, format_description, trigger_typing, + escape_code_block, ) logger = getLogger(__name__) @@ -155,8 +156,10 @@ async def snippet(self, ctx, *, name: str.lower = None): val = self.bot.snippets.get(name) if val is None: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") - return await ctx.send(embed=embed) - return await ctx.send(escape_mentions(val)) + else: + embed = discord.Embed(color=self.bot.main_color) + embed.add_field(name=f"`{name}` will send:", value=val) + return await ctx.send(embed=embed) if not self.bot.snippets: embed = discord.Embed( @@ -186,8 +189,11 @@ async def snippet_raw(self, ctx, *, name: str.lower): val = self.bot.snippets.get(name) if val is None: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") - return await ctx.send(embed=embed) - return await ctx.send(escape_markdown(escape_mentions(val)).replace("<", "\\<")) + else: + embed = discord.Embed(color=self.bot.main_color) + val = escape_code_block(val) + embed.add_field(name=f"`{name}` will send:", value=f"```\n{val}```") + return await ctx.send(embed=embed) @snippet.command(name="add") @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -782,10 +788,32 @@ async def reply(self, ctx, *, msg: str = ""): async with ctx.typing(): await ctx.thread.reply(ctx.message) - @commands.command() + @commands.command(aliases=["formatreply"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def freply(self, ctx, *, msg: str = ""): + """ + Reply to a Modmail thread with variables. + + Works just like `{prefix}reply`, however with the addition of three variables: + - `{channel}` - the `discord.TextChannel` object + - `{recipient}` - the `discord.User` object of the recipient + - `{author}` - the `discord.User` object of the author + + Supports attachments and images as well as + automatically embedding image URLs. + """ + msg = self.bot.formatter.format( + msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author + ) + ctx.message.content = msg + async with ctx.typing(): + await ctx.thread.reply(ctx.message) + + @commands.command(aliases=["anonreply", "anonymousreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def anonreply(self, ctx, *, msg: str = ""): + async def areply(self, ctx, *, msg: str = ""): """ Reply to a thread anonymously. @@ -823,11 +851,10 @@ async def find_linked_message(self, ctx, message_id): continue # TODO: use regex to find the linked message id linked_message_id = str(embed.author.url).split("/")[-1] - break + elif message_id and msg.id == message_id: url = msg.embeds[0].author.url linked_message_id = str(url).split("/")[-1] - break return linked_message_id diff --git a/cogs/plugins.py b/cogs/plugins.py index 8060799ce2..e4543d2f1e 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -539,12 +539,12 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N return await ctx.send(embed=embed) - for plugin_name, details in registry: - details = self.registry[plugin_name] + for name, details in registry: + details = self.registry[name] user, repo = details["repository"].split("/", maxsplit=1) branch = details.get("branch") - plugin = Plugin(user, repo, plugin_name, branch) + plugin = Plugin(user, repo, name, branch) embed = discord.Embed( color=self.bot.main_color, @@ -554,7 +554,7 @@ 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 {name}```" ) embed.set_author( diff --git a/cogs/utility.py b/cogs/utility.py index 4f44856a8e..4282ed22a8 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -17,7 +17,6 @@ from discord.enums import ActivityType, Status from discord.ext import commands, tasks from discord.ext.commands.view import StringView -from discord.utils import escape_markdown, escape_mentions from aiohttp import ClientResponseError from pkg_resources import parse_version @@ -161,28 +160,41 @@ async def send_error_message(self, error): command = self.context.kwargs.get("command") val = self.context.bot.snippets.get(command) if val is not None: - return await self.get_destination().send( - escape_mentions(f"**`{command}` is a snippet, content:**\n\n{val}") + embed = discord.Embed( + title=f"{command} is a snippet.", color=self.context.bot.main_color ) + embed.add_field(name=f"`{command}` will send:", value=val) + return await self.get_destination().send(embed=embed) val = self.context.bot.aliases.get(command) if val is not None: values = utils.parse_alias(val) - if len(values) == 1: + if not values: embed = discord.Embed( - title=f"{command} is an alias.", - color=self.context.bot.main_color, - description=f"`{command}` points to `{escape_markdown(values[0])}`.", + title="Error", + color=self.context.bot.error_color, + description=f"Alias `{command}` is invalid, this alias will now be deleted." + "This alias will now be deleted.", ) + embed.add_field(name=f"{command}` used to be:", value=val) + self.context.bot.aliases.pop(command) + await self.context.bot.config.update() else: - embed = discord.Embed( - title=f"{command} is an alias.", - color=self.context.bot.main_color, - description=f"**`{command}` points to the following steps:**", - ) - for i, val in enumerate(values, start=1): - embed.description += f"\n{i}: {escape_markdown(val)}" + if len(values) == 1: + embed = discord.Embed( + title=f"{command} is an alias.", color=self.context.bot.main_color + ) + embed.add_field(name=f"`{command}` points to:", value=values[0]) + else: + embed = discord.Embed( + title=f"{command} is an alias.", + color=self.context.bot.main_color, + description=f"**`{command}` points to the following steps:**", + ) + for i, val in enumerate(values, start=1): + embed.add_field(name=f"Step {i}:", value=val) + embed.set_footer( text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" ' "for more details on aliases." @@ -901,25 +913,24 @@ async def alias(self, ctx, *, name: str.lower = None): embed = discord.Embed( title="Error", color=self.bot.error_color, - description=f"Alias `{name}` is invalid, it used to be `{escape_markdown(val)}`. " + description=f"Alias `{name}` is invalid, this alias will now be deleted." "This alias will now be deleted.", ) + embed.add_field(name=f"{name}` used to be:", value=val) self.bot.aliases.pop(name) await self.bot.config.update() return await ctx.send(embed=embed) if len(values) == 1: - embed = discord.Embed( - color=self.bot.main_color, - description=f"`{name}` points to `{escape_markdown(values[0])}`.", - ) + embed = discord.Embed(color=self.bot.main_color) + embed.add_field(name=f"`{name}` points to:", value=values[0]) else: embed = discord.Embed( color=self.bot.main_color, description=f"**`{name}` points to the following steps:**", ) for i, val in enumerate(values, start=1): - embed.description += f"\n{i}: {escape_markdown(val)}" + embed.add_field(name=f"Step {i}:", value=val) return await ctx.send(embed=embed) @@ -927,7 +938,7 @@ async def alias(self, ctx, *, name: str.lower = None): 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.") + embed.set_footer(text=f'Do "{self.bot.prefix}help alias" for more commands.') embed.set_author(name="Aliases", icon_url=ctx.guild.icon_url) return await ctx.send(embed=embed) @@ -952,56 +963,13 @@ async def alias_raw(self, ctx, *, name: str.lower): if val is None: embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias") return await ctx.send(embed=embed) - return await ctx.send(escape_markdown(escape_mentions(val)).replace("<", "\\<")) - - @alias.command(name="add") - @checks.has_permissions(PermissionLevel.MODERATOR) - async def alias_add(self, ctx, name: str.lower, *, value): - """ - Add an alias. - - Alias also supports multi-step aliases, to create a multi-step alias use quotes - to wrap each step and separate each step with `&&`. For example: - - - `{prefix}alias add movenreply "move admin-category" && "reply Thanks for reaching out to the admins"` - - However, if you run into problems, try wrapping the command with quotes. For example: - - - This will fail: `{prefix}alias add reply You'll need to type && to work` - - Correct method: `{prefix}alias add reply "You'll need to type && to work"` - """ - embed = None - if self.bot.get_command(name): - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"A command with the same name already exists: `{name}`.", - ) - elif name in self.bot.aliases: - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"Another alias with the same name already exists: `{name}`.", - ) - - elif name in self.bot.snippets: - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"A snippet with the same name already exists: `{name}`.", - ) - - elif len(name) > 120: - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description="Alias names cannot be longer than 120 characters.", - ) - - if embed is not None: - return await ctx.send(embed=embed) + embed = discord.Embed(color=self.bot.main_color) + val = utils.escape_code_block(val) + embed.add_field(name=f"`{name}` points to:", value=f"```\n{val}```") + return await ctx.send(embed=embed) + async def make_alias(self, name, value, action): values = utils.parse_alias(value) save_aliases = [] @@ -1012,14 +980,14 @@ async def alias_add(self, ctx, name: str.lower, *, value): 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.') - return await ctx.send(embed=embed) + return embed multiple_alias = len(values) > 1 - embed = discord.Embed(title="Added alias", color=self.bot.main_color) + embed = discord.Embed(title=f"{action} alias", color=self.bot.main_color) if not multiple_alias: - embed.description = f'`{name}` points to: "{values[0]}".' + embed.add_field(name=f"`{name}` points to:", value=values[0]) else: embed.description = f"`{name}` now points to the following steps:" @@ -1043,17 +1011,66 @@ async def alias_add(self, ctx, name: str.lower, *, value): else: embed.description = ( "The command you are attempting to point " - f"to in step {i} does not exist: `{linked_command}`." + f"to on step {i} does not exist: `{linked_command}`." ) - return await ctx.send(embed=embed) + return embed else: save_aliases.append(val) - - embed.description += f"\n{i}: {val}" + if multiple_alias: + embed.add_field(name=f"Step {i}:", value=val) self.bot.aliases[name] = " && ".join(f'"{a}"' for a in save_aliases) await self.bot.config.update() + return embed + + @alias.command(name="add") + @checks.has_permissions(PermissionLevel.MODERATOR) + async def alias_add(self, ctx, name: str.lower, *, value): + """ + Add an alias. + + Alias also supports multi-step aliases, to create a multi-step alias use quotes + to wrap each step and separate each step with `&&`. For example: + + - `{prefix}alias add movenreply "move admin-category" && "reply Thanks for reaching out to the admins"` + + However, if you run into problems, try wrapping the command with quotes. For example: + + - This will fail: `{prefix}alias add reply You'll need to type && to work` + - Correct method: `{prefix}alias add reply "You'll need to type && to work"` + """ + embed = None + if self.bot.get_command(name): + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description=f"A command with the same name already exists: `{name}`.", + ) + + elif name in self.bot.aliases: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description=f"Another alias with the same name already exists: `{name}`.", + ) + + elif name in self.bot.snippets: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description=f"A snippet with the same name already exists: `{name}`.", + ) + + elif len(name) > 120: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="Alias names cannot be longer than 120 characters.", + ) + + if embed is None: + embed = await self.make_alias(name, value, "Added") return await ctx.send(embed=embed) @alias.command(name="remove", aliases=["del", "delete"]) @@ -1085,58 +1102,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value): embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias") return await ctx.send(embed=embed) - values = utils.parse_alias(value) - save_aliases = [] - - if not values: - embed = discord.Embed( - title="Error", - 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.') - return await ctx.send(embed=embed) - - multiple_alias = len(values) > 1 - - embed = discord.Embed(title="Edited alias", color=self.bot.main_color) - - if not multiple_alias: - embed.description = f'`{name}` points to: "{values[0]}".' - else: - embed.description = f"`{name}` now points to the following steps:" - - for i, val in enumerate(values, start=1): - view = StringView(val) - linked_command = view.get_word().lower() - message = view.read_rest() - - if not self.bot.get_command(linked_command): - alias_command = self.bot.aliases.get(linked_command) - if alias_command is not None: - save_aliases.extend(utils.normalize_alias(alias_command, message)) - else: - embed = discord.Embed(title="Error", color=self.bot.error_color) - - if multiple_alias: - embed.description = ( - "The command you are attempting to point " - f"to does not exist: `{linked_command}`." - ) - else: - embed.description = ( - "The command you are attempting to point " - f"to n step {i} does not exist: `{linked_command}`." - ) - - return await ctx.send(embed=embed) - else: - save_aliases.append(val) - - embed.description += f"\n{i}: {val}" - - self.bot.aliases[name] = " && ".join(f'"{a}"' for a in save_aliases) - await self.bot.config.update() + embed = await self.make_alias(name, value, "Edited") return await ctx.send(embed=embed) @commands.group(aliases=["perms"], invoke_without_command=True) diff --git a/core/clients.py b/core/clients.py index d8814335bf..bb17d7c680 100644 --- a/core/clients.py +++ b/core/clients.py @@ -235,9 +235,7 @@ async def append_log( async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: return await self.logs.find_one_and_update( - {"channel_id": str(channel_id)}, - {"$set": {k: v for k, v in data.items()}}, - return_document=True, + {"channel_id": str(channel_id)}, {"$set": data}, return_document=True ) diff --git a/core/utils.py b/core/utils.py index 44b890f230..5b61b94642 100644 --- a/core/utils.py +++ b/core/utils.py @@ -226,11 +226,17 @@ def decode_alias(m): r"(?:(?:\s*(? Date: Tue, 3 Dec 2019 11:29:36 -0800 Subject: [PATCH 24/38] Fixed ?help freply --- cogs/modmail.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 29f4132149..7a40dbe040 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -796,9 +796,9 @@ async def freply(self, ctx, *, msg: str = ""): Reply to a Modmail thread with variables. Works just like `{prefix}reply`, however with the addition of three variables: - - `{channel}` - the `discord.TextChannel` object - - `{recipient}` - the `discord.User` object of the recipient - - `{author}` - the `discord.User` object of the author + - `{{channel}}` - the `discord.TextChannel` object + - `{{recipient}}` - the `discord.User` object of the recipient + - `{{author}}` - the `discord.User` object of the author Supports attachments and images as well as automatically embedding image URLs. From 9385f37483ad0e547083a2538345a8da707fa254 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 4 Dec 2019 02:39:35 -0800 Subject: [PATCH 25/38] v3.3.2-dev5 - fix edit/delete --- .github/workflows/stale.yml | 3 +- CHANGELOG.md | 11 +- bot.py | 57 ++++--- cogs/modmail.py | 170 ++++++++++++++------ core/clients.py | 11 +- core/thread.py | 308 ++++++++++++++++++++++-------------- core/utils.py | 22 ++- 7 files changed, 390 insertions(+), 192 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 75f607f8e2..b3003ccf92 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,5 @@ -name: "Close stale issues" +name: "Close Stale Issues" + on: schedule: - cron: "0 0 * * *" diff --git a/CHANGELOG.md b/CHANGELOG.md index 54389ce6f4..f253d9a9eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ 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.2-dev4 +# v3.3.2-dev5 + +(Development update, very likely to be unstable!) ### Added @@ -21,20 +23,27 @@ however, insignificant breaking changes does not guarantee a major version bump, - Added "perhaps you meant" section to `?config help`. - Multi-command alias is now more stable. With support for a single quote escape `\"`. - New command `?freply`, which behaves exactly like `?reply` with the addition that you can substitute `{channel}`, `{recipient}`, and `{author}` to be their respective values. +- New command `?repair`, repair any broken Modmail thread (with help from @officialpiyush). +- Recipients gets a feedback when they edit message. ### Changed - The look of alias and snippet when previewing. +- Message ID of the thread embed is saved in DB, instead of the original message. ### Fixed - Setting config vars using human time wasn't working. - Fixed some bugs with aliases. +- Fixed a lot of issues with `?edit` and `?delete` and recipient message edit. +- Masked the error: "AttributeError: 'int' object has no attribute 'name'" + - Channel delete event will not be checked until discord.py fixes this issue. ### Internal - Commit to black format line width max = 99, consistent with pylint. - Alias parser is rewritten without shlex. +- New checks with thread create / find. # v3.3.1 diff --git a/bot.py b/bot.py index 5a9ea676e3..2c981d26a8 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.2-dev4" +__version__ = "3.3.2-dev5" import asyncio @@ -150,7 +150,7 @@ def session(self) -> ClientSession: return self._session @property - def api(self): + def api(self) -> ApiClient: if self._api is None: self._api = ApiClient(self) return self._api @@ -435,8 +435,13 @@ async def on_ready(self): for recipient_id, items in tuple(closures.items()): after = (datetime.fromisoformat(items["time"]) - datetime.utcnow()).total_seconds() - if after < 0: + if after <= 0: + logger.debug("Closing thread for recipient %s.", recipient_id) after = 0 + else: + logger.debug( + "Thread for recipient %s will be closed after %s seconds.", recipient_id, after + ) thread = await self.threads.find(recipient_id=int(recipient_id)) @@ -447,8 +452,6 @@ async def on_ready(self): await self.config.update() continue - logger.debug("Closing thread for recipient %s.", recipient_id) - await thread.close( closer=self.get_user(items["closer_id"]), after=after, @@ -977,15 +980,20 @@ async def on_guild_channel_delete(self, channel): if channel.guild != self.modmail_guild: return - audit_logs = self.modmail_guild.audit_logs() - entry = await audit_logs.find(lambda e: e.target.id == channel.id) - mod = entry.user + try: + audit_logs = self.modmail_guild.audit_logs() + entry = await audit_logs.find(lambda a: a.target == channel) + mod = entry.user + except AttributeError as e: + # discord.py broken implementation with discord API + logger.warning("Failed to retrieve audit log.", str(e)) + return if mod == self.user: return if isinstance(channel, discord.CategoryChannel): - if self.main_category.id == channel.id: + if self.main_category == channel: logger.debug("Main category was deleted.") self.config.remove("main_category_id") await self.config.update() @@ -994,14 +1002,14 @@ async def on_guild_channel_delete(self, channel): if not isinstance(channel, discord.TextChannel): return - if self.log_channel is None or self.log_channel.id == channel.id: + if self.log_channel is None or self.log_channel == channel: logger.info("Log channel deleted.") self.config.remove("log_channel_id") await self.config.update() return thread = await self.threads.find(channel=channel) - if thread: + if thread and thread.channel == channel: logger.debug("Manually closed channel %s.", channel.name) await thread.close(closer=mod, silent=True, delete_channel=False) @@ -1044,19 +1052,24 @@ async def on_bulk_message_delete(self, messages): await discord.utils.async_all(self.on_message_delete(msg) for msg in messages) async def on_message_edit(self, before, after): - if before.author.bot: + if after.author.bot: return - if isinstance(before.channel, discord.DMChannel): + if isinstance(after.channel, discord.DMChannel): thread = await self.threads.find(recipient=before.author) - async for msg in thread.channel.history(): - if msg.embeds: - embed = msg.embeds[0] - matches = str(embed.author.url).split("/") - if matches and matches[-1] == str(before.id): - embed.description = after.content - await msg.edit(embed=embed) - await self.api.edit_message(str(after.id), after.content) - break + try: + await thread.edit_dm_message(after, after.content) + except ValueError: + _, blocked_emoji = await self.retrieve_emoji() + try: + await after.add_reaction(blocked_emoji) + except (discord.HTTPException, discord.InvalidArgument): + pass + else: + embed = discord.Embed( + description="Successfully Edited Message", color=self.main_color + ) + embed.set_footer(text=f"Message ID: {after.id}") + await after.channel.send(embed=embed) async def on_error(self, event_method, *args, **kwargs): logger.error("Ignoring exception in %s.", event_method) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7a40dbe040..25bb0366b8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,4 +1,5 @@ import asyncio +import re from datetime import datetime from itertools import zip_longest from typing import Optional, Union @@ -14,6 +15,7 @@ from core import checks from core.models import PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession +from core.thread import Thread from core.time import UserFriendlyTime, human_timedelta from core.utils import ( format_preview, @@ -22,6 +24,8 @@ format_description, trigger_typing, escape_code_block, + match_user_id, + format_channel_name, ) logger = getLogger(__name__) @@ -841,23 +845,6 @@ async def note(self, ctx, *, msg: str = ""): msg = await ctx.thread.note(ctx.message) await msg.pin() - async def find_linked_message(self, ctx, message_id): - linked_message_id = None - - async for msg in ctx.channel.history(): - if message_id is None and msg.embeds: - embed = msg.embeds[0] - 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] - - elif message_id and msg.id == message_id: - url = msg.embeds[0].author.url - linked_message_id = str(url).split("/")[-1] - - return linked_message_id - @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @@ -867,12 +854,14 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): If no `message_id` is provided, the last message sent by a staff will be edited. + + Note: attachments **cannot** be edited. """ thread = ctx.thread - linked_message_id = await self.find_linked_message(ctx, message_id) - - if linked_message_id is None: + try: + await thread.edit_message(message_id, message) + except ValueError: return await ctx.send( embed=discord.Embed( title="Failed", @@ -881,16 +870,8 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): ) ) - await asyncio.gather( - thread.edit_message(linked_message_id, message), - self.bot.api.edit_message(linked_message_id, message), - ) - sent_emoji, _ = await self.bot.retrieve_emoji() - try: - await ctx.message.add_reaction(sent_emoji) - except (discord.HTTPException, discord.InvalidArgument): - pass + return await ctx.message.add_reaction(sent_emoji) @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1168,7 +1149,7 @@ async def unblock(self, ctx, *, user: User = None): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def delete(self, ctx, message_id: Optional[int] = None): + async def delete(self, ctx, message_id: int = None): """ Delete a message that was sent using the reply command or a note. @@ -1179,15 +1160,9 @@ async def delete(self, ctx, message_id: Optional[int] = None): """ thread = ctx.thread - if message_id is not None: - try: - message_id = int(message_id) - except ValueError: - raise commands.BadArgument("A message ID needs to be specified.") - - linked_message_id = await self.find_linked_message(ctx, message_id) - - if linked_message_id is None: + try: + await thread.delete_message(message_id) + except ValueError: return await ctx.send( embed=discord.Embed( title="Failed", @@ -1196,12 +1171,119 @@ async def delete(self, ctx, message_id: Optional[int] = None): ) ) - await thread.delete_message(linked_message_id) sent_emoji, _ = await self.bot.retrieve_emoji() - try: - await ctx.message.add_reaction(sent_emoji) - except (discord.HTTPException, discord.InvalidArgument): - pass + return await ctx.message.add_reaction(sent_emoji) + + @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def repair(self, ctx): + """ + Repair a thread broken by Discord. + """ + sent_emoji, blocked_emoji = await self.bot.retrieve_emoji() + + if ctx.thread: + user_id = match_user_id(ctx.channel.topic) + if user_id == -1: + logger.info("Setting current channel's topic to User ID.") + await ctx.channel.edit(topic=f"User ID: {ctx.thread.id}") + return await ctx.message.add_reaction(sent_emoji) + + logger.info("Attempting to fix a broken thread %s.", ctx.channel.name) + + # Search cache for channel + user_id, thread = next( + ((k, v) for k, v in self.bot.threads.cache.items() if v.channel == ctx.channel), + (-1, None), + ) + if thread is not None: + logger.debug("Found thread with tempered ID.") + await ctx.channel.edit(reason="Fix broken Modmail thread", topic=f"User ID: {user_id}") + return await ctx.message.add_reaction(sent_emoji) + + # find genesis message to retrieve User ID + async for message in ctx.channel.history(limit=10, oldest_first=True): + if ( + message.author == self.bot.user + and message.embeds + and message.embeds[0].color + and message.embeds[0].color.value == self.bot.main_color + and message.embeds[0].footer.text + ): + user_id = match_user_id(message.embeds[0].footer.text) + if user_id != -1: + recipient = self.bot.get_user(user_id) + if recipient is None: + self.bot.threads.cache[user_id] = thread = Thread( + self.bot.threads, user_id, ctx.channel + ) + else: + self.bot.threads.cache[user_id] = thread = Thread( + self.bot.threads, recipient, ctx.channel + ) + thread.ready = True + logger.info( + "Setting current channel's topic to User ID and created new thread." + ) + await ctx.channel.edit( + reason="Fix broken Modmail thread", topic=f"User ID: {user_id}" + ) + return await ctx.message.add_reaction(sent_emoji) + + else: + logger.warning("No genesis message found.") + + # match username from channel name + # username-1234, username-1234_1, username-1234_2 + m = re.match(r"^(.+)-(\d{4})(?:_\d+)?$", ctx.channel.name) + if m is not None: + users = set( + filter( + lambda member: member.name == m.group(1) + and member.discriminator == m.group(2), + ctx.guild.members, + ) + ) + if len(users) == 1: + user = users[0] + name = format_channel_name( + user, self.bot.modmail_guild, exclude_channel=ctx.channel + ) + recipient = self.bot.get_user(user.id) + if user.id in self.bot.threads.cache: + thread = self.bot.threads.cache[user.id] + if thread.channel: + embed = discord.Embed( + title="Delete Channel", + description="This thread channel is no longer in use. " + f"All messages will be directed to {ctx.channel.mention} instead.", + color=self.bot.error_color, + ) + embed.set_footer( + text='Please manually delete this channel, do not use "{prefix}close".' + ) + try: + await thread.channel.send(embed=embed) + except discord.HTTPException: + pass + if recipient is None: + self.bot.threads.cache[user.id] = thread = Thread( + self.bot.threads, user_id, ctx.channel + ) + else: + self.bot.threads.cache[user.id] = thread = Thread( + self.bot.threads, recipient, ctx.channel + ) + thread.ready = True + logger.info("Setting current channel's topic to User ID and created new thread.") + await ctx.channel.edit( + reason="Fix broken Modmail thread", name=name, topic=f"User ID: {user.id}" + ) + return await ctx.message.add_reaction(sent_emoji) + + elif len(users) >= 2: + logger.info("Multiple users with the same name and discriminator.") + return await ctx.message.add_reaction(blocked_emoji) @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) diff --git a/core/clients.py b/core/clients.py index bb17d7c680..54cc28c9be 100644 --- a/core/clients.py +++ b/core/clients.py @@ -202,12 +202,19 @@ async def edit_message(self, message_id: Union[int, str], new_content: str) -> N ) async def append_log( - self, message: Message, channel_id: Union[str, int] = "", type_: str = "thread_message" + self, + message: Message, + *, + message_id: str = "", + channel_id: str = "", + type_: str = "thread_message", ) -> dict: channel_id = str(channel_id) or str(message.channel.id) + message_id = str(message_id) or str(message.id) + data = { "timestamp": str(message.created_at), - "message_id": str(message.id), + "message_id": message_id, "author": { "id": str(message.author.id), "name": message.author.name, diff --git a/core/thread.py b/core/thread.py index 79651491c5..236576a281 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,6 +1,5 @@ import asyncio import re -import string import typing from datetime import datetime, timedelta from types import SimpleNamespace @@ -12,7 +11,7 @@ from core.models import getLogger from core.time import human_timedelta -from core.utils import is_image_url, days, match_user_id, truncate +from core.utils import is_image_url, days, match_user_id, truncate, format_channel_name logger = getLogger(__name__) @@ -47,7 +46,11 @@ def __repr__(self): async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" - await self._ready_event.wait() + # timeout after 3 seconds + try: + await asyncio.wait_for(self._ready_event.wait(), timeout=3) + except asyncio.TimeoutError: + return @property def id(self) -> int: @@ -92,7 +95,7 @@ async def setup(self, *, creator=None, category=None): try: channel = await self.bot.modmail_guild.create_text_channel( - name=self.manager.format_channel_name(recipient), + name=format_channel_name(recipient, self.bot.modmail_guild), category=category, overwrites=overwrites, reason="Creating a thread channel.", @@ -124,6 +127,9 @@ async def setup(self, *, creator=None, category=None): log_url = log_count = None # ensure core functionality still works + await channel.edit(topic=f"User ID: {recipient.id}") + self.ready = True + if creator: mention = None else: @@ -139,38 +145,36 @@ async def send_genesis_message(): self.genesis_message = msg except Exception: logger.error("Failed unexpectedly:", exc_info=True) - finally: - self.ready = True - await channel.edit(topic=f"User ID: {recipient.id}") - self.bot.loop.create_task(send_genesis_message()) + async def send_recipient_genesis_message(): + # Once thread is ready, tell the recipient. + thread_creation_response = self.bot.config["thread_creation_response"] - # Once thread is ready, tell the recipient. - thread_creation_response = self.bot.config["thread_creation_response"] + embed = discord.Embed( + color=self.bot.mod_color, + description=thread_creation_response, + timestamp=channel.created_at, + ) - embed = discord.Embed( - color=self.bot.mod_color, - description=thread_creation_response, - timestamp=channel.created_at, - ) + recipient_thread_close = self.bot.config.get("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"] + else: + footer = self.bot.config["thread_creation_footer"] - if recipient_thread_close: - footer = self.bot.config["thread_self_closable_creation_footer"] - else: - footer = self.bot.config["thread_creation_footer"] + embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) + embed.title = self.bot.config["thread_creation_title"] - embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) - embed.title = self.bot.config["thread_creation_title"] + if creator is None: + msg = await recipient.send(embed=embed) - if creator is None: - msg = await recipient.send(embed=embed) + if recipient_thread_close: + close_emoji = self.bot.config["close_emoji"] + close_emoji = await self.bot.convert_emoji(close_emoji) + await msg.add_reaction(close_emoji) - if recipient_thread_close: - close_emoji = self.bot.config["close_emoji"] - close_emoji = await self.bot.convert_emoji(close_emoji) - await msg.add_reaction(close_emoji) + await asyncio.gather(send_genesis_message(), send_recipient_genesis_message()) def _format_info_embed(self, user, log_url, log_count, color): """Get information about a member of a server @@ -188,7 +192,8 @@ def _format_info_embed(self, user, log_url, log_count, color): roles = [] for role in sorted(member.roles, key=lambda r: r.position): - if role.name == "@everyone": + if role.is_default(): + # @everyone continue fmt = role.name if sep_server else role.mention @@ -395,9 +400,7 @@ async def _close( await asyncio.gather(*tasks) - async def cancel_closure( - self, auto_close: bool = False, all: bool = False # pylint: disable=redefined-builtin - ) -> None: + async def cancel_closure(self, auto_close: bool = False, all: bool = False) -> None: if self.close_task is not None and (not auto_close or all): self.close_task.cancel() self.close_task = None @@ -409,31 +412,12 @@ async def cancel_closure( if to_update is not None: await self.bot.config.update() - @staticmethod - async def _find_thread_message(channel, message_id): - async for msg in channel.history(): - if not msg.embeds: - continue - embed = msg.embeds[0] - if embed and embed.author and embed.author.url: - if str(message_id) == str(embed.author.url).split("/")[-1]: - return msg - - async def _fetch_timeout(self) -> typing.Union[None, isodate.duration.Duration, timedelta]: - """ - 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") - return timeout - async def _restart_close_timer(self): """ This will create or restart a timer to automatically close this thread. """ - timeout = await self._fetch_timeout() + timeout = self.bot.config.get("thread_auto_close") # Exit if timeout was not set if timeout == isodate.Duration(): @@ -468,38 +452,118 @@ async def _restart_close_timer(self): closer=self.bot.user, after=int(seconds), message=close_message, auto_close=True ) - async def edit_message(self, message_id: int, message: str) -> None: - recipient_msg, channel_msg = await asyncio.gather( - self._find_thread_message(self.recipient, message_id), - self._find_thread_message(self.channel, message_id), - ) + async def find_linked_messages( + self, message_id: typing.Optional[int] = None + ) -> typing.Tuple[discord.Message, typing.Optional[discord.Message]]: + if message_id is not None: + try: + message1 = await self.channel.fetch_message(message_id) + except discord.NotFound: + raise ValueError("Thread message not found.") + + if not ( + message1.embeds and message1.embeds[0].author.url and message1.embeds[0].color + ): + raise ValueError("Thread message not found.") + + if message1.embeds[0].color.value == self.bot.main_color and message1.embeds[ + 0 + ].author.name.startswith("Note"): + return message1, None + + if message1.embeds[0].color.value != self.bot.mod_color: + raise ValueError("Thread message not found.") + else: + async for message1 in self.channel.history(): + if ( + message1.embeds + and message1.embeds[0].author.url + and message1.embeds[0].color + and message1.embeds[0].color.value == self.bot.mod_color + ): + break + else: + raise ValueError("Thread message not found.") + + try: + joint_id = int(message1.embeds[0].author.url.split("#")[-1]) + except ValueError: + raise ValueError("Malformed thread message.") + + async for msg in self.recipient.history(): + if not (msg.embeds and msg.embeds[0].author.url): + continue + try: + if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id: + return message1, msg + except ValueError: + raise ValueError("DM message not found.") + raise ValueError("DM message not found.") + + async def edit_message(self, message_id: typing.Optional[int], message: str) -> None: + try: + message1, message2 = await self.find_linked_messages(message_id) + except ValueError: + logger.warning("Failed to edit message.", exc_info=True) + raise + + embed1 = message1.embeds[0] + embed1.description = message - channel_embed = channel_msg.embeds[0] - channel_embed.description = message + tasks = [self.bot.api.edit_message(message1.id, message), message1.edit(embed=embed1)] + if message2 is not None: + embed2 = message2.embeds[0] + embed2.description = message + tasks += [message2.edit(embed=embed2)] - tasks = [channel_msg.edit(embed=channel_embed)] + await asyncio.gather(*tasks) - if recipient_msg: - recipient_embed = recipient_msg.embeds[0] - recipient_embed.description = message - tasks.append(recipient_msg.edit(embed=recipient_embed)) + async def delete_message(self, message_id: typing.Optional[int]) -> None: + try: + message1, message2 = await self.find_linked_messages(message_id) + except ValueError: + logger.warning("Failed to delete message.", exc_info=True) + raise + tasks = [message1.delete()] + if message2 is not None: + tasks += [message2.delete()] await asyncio.gather(*tasks) - async def delete_message(self, message_id): - msg_recipient, msg_channel = await asyncio.gather( - self._find_thread_message(self.recipient, message_id), - self._find_thread_message(self.channel, message_id), + async def edit_dm_message(self, message: discord.Message, content: str) -> None: + async for linked_message in self.channel.history(): + if not linked_message.embeds: + continue + url = linked_message.embeds[0].author.url + if not url: + continue + msg_id = url.split("#")[-1] + try: + if int(msg_id) == message.id: + break + except ValueError: + logger.warning("Failed to edit message.", exc_info=True) + raise ValueError("Malformed current channel message.") + else: + logger.warning("Failed to edit message.", exc_info=True) + raise ValueError("Current channel message not found.") + embed = linked_message.embeds[0] + embed.add_field(name="**Edited, former message:**", value=embed.description) + embed.description = content + await asyncio.gather( + self.bot.api.edit_message(message.id, content), linked_message.edit(embed=embed) ) - await asyncio.gather(msg_recipient.delete(), msg_channel.delete()) async def note(self, message: discord.Message) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) - _, msg = await asyncio.gather( - self.bot.api.append_log(message, self.channel.id, type_="system"), - self.send(message, self.channel, note=True), + msg = await self.send(message, self.channel, note=True) + + self.bot.loop.create_task( + self.bot.api.append_log( + message, message_id=msg.id, channel_id=self.channel.id, type_="system" + ) ) return msg @@ -537,13 +601,16 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None ) else: # Send the same thing in the thread channel. - tasks.append( - self.send(message, destination=self.channel, from_mod=True, anonymous=anonymous) + msg = await self.send( + message, destination=self.channel, from_mod=True, anonymous=anonymous ) tasks.append( self.bot.api.append_log( - message, self.channel.id, type_="anonymous" if anonymous else "thread_message" + message, + message_id=msg.id, + channel_id=self.channel.id, + type_="anonymous" if anonymous else "thread_message", ) ) @@ -592,7 +659,7 @@ async def send( await self.wait_until_ready() if not from_mod and not note: - self.bot.loop.create_task(self.bot.api.append_log(message, self.channel.id)) + self.bot.loop.create_task(self.bot.api.append_log(message, channel_id=self.channel.id)) destination = destination or self.channel @@ -607,7 +674,7 @@ async def send( # Anonymously sending to the user. tag = self.bot.config["mod_tag"] if tag is None: - tag = str(message.author.top_role) + tag = str(author.top_role) name = self.bot.config["anon_username"] if name is None: name = tag @@ -617,21 +684,23 @@ async def send( embed.set_author( name=name, icon_url=avatar_url, - url=f"https://discordapp.com/channels/{self.bot.guild.id}", + url=f"https://discordapp.com/channels/{self.bot.guild.id}#{message.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}" + name=name, + icon_url=avatar_url, + url=f"https://discordapp.com/users/{author.id}#{message.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}#{message.id}", ) ext = [(a.url, a.filename) for a in message.attachments] @@ -763,11 +832,6 @@ def __init__(self, bot): async def populate_cache(self) -> None: for channel in self.bot.modmail_guild.text_channels: - if ( - channel.category != self.bot.main_category - and not self.bot.using_multiple_server_setup - ): - continue await self.find(channel=channel) def __len__(self): @@ -798,19 +862,21 @@ async def find( await channel.edit(topic=f"User ID: {user_id}") return thread - thread = None - if recipient: recipient_id = recipient.id - try: - return self.cache[recipient_id] - # if not thread.channel or not self.bot.get_channel(thread.channel.id): - # self.bot.loop.create_task( - # thread.close(closer=self.bot.user, silent=True, delete_channel=False) - # ) - # thread = None - except KeyError: + thread = self.cache.get(recipient_id) + if thread is not None: + await thread.wait_until_ready() + if not thread.channel or not self.bot.get_channel(thread.channel.id): + logger.warning( + "Found existing thread for %s but the channel is invalid.", recipient_id + ) + self.bot.loop.create_task( + thread.close(closer=self.bot.user, silent=True, delete_channel=False) + ) + thread = None + else: channel = discord.utils.get( self.bot.modmail_guild.text_channels, topic=f"User ID: {recipient_id}" ) @@ -832,19 +898,20 @@ def _find_from_channel(self, channel): if channel.topic: user_id = match_user_id(channel.topic) - if user_id != -1: - if user_id in self.cache: - return self.cache[user_id] + if user_id == -1: + return None - recipient = self.bot.get_user(user_id) - if recipient is None: - self.cache[user_id] = thread = Thread(self, user_id, channel) - else: - self.cache[user_id] = thread = Thread(self, recipient, channel) - thread.ready = True + if user_id in self.cache: + return self.cache[user_id] - return thread - return None + recipient = self.bot.get_user(user_id) + if recipient is None: + self.cache[user_id] = thread = Thread(self, user_id, channel) + else: + self.cache[user_id] = thread = Thread(self, recipient, channel) + thread.ready = True + + return thread async def create( self, @@ -854,8 +921,21 @@ async def create( category: discord.CategoryChannel = None, ) -> Thread: """Creates a Modmail thread""" - # create thread immediately so messages can be processed + + # checks for existing thread in cache + thread = self.cache.get(recipient.id) + if thread: + await thread.wait_until_ready() + if thread.channel and self.bot.get_channel(thread.channel.id): + logger.warning("Found an existing thread for %s, abort creating.", recipient) + return thread + logger.warning("Found an existing thread for %s, closing previous thread.", recipient) + self.bot.loop.create_task( + thread.close(closer=self.bot.user, silent=True, delete_channel=False) + ) + thread = Thread(self, recipient) + self.cache[recipient.id] = thread # Schedule thread setup for later @@ -877,17 +957,3 @@ async def create( async def find_or_create(self, recipient) -> Thread: return await self.find(recipient=recipient) or await self.create(recipient) - - def format_channel_name(self, author): - """Sanitises a username for use with text channel names""" - name = author.name.lower() - new_name = ( - "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" - ) - new_name += f"-{author.discriminator}" - - counter = 1 - while new_name in [c.name for c in self.bot.modmail_guild.text_channels]: - new_name += f"-{counter}" # two channels with same name - - return new_name diff --git a/core/utils.py b/core/utils.py index 5b61b94642..a3b38d63c8 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,6 +1,7 @@ import base64 import functools import re +import string import typing from difflib import get_close_matches from distutils.util import strtobool as _stb # pylint: disable=import-error @@ -183,6 +184,9 @@ def cleanup_code(content: str) -> str: return content.strip("` \n") +TOPIC_REGEX = re.compile(r"\bUser ID:\s*(\d{17,21})\b", flags=re.IGNORECASE) + + def match_user_id(text: str) -> int: """ Matches a user ID in the format of "User ID: 12345". @@ -197,7 +201,7 @@ def match_user_id(text: str) -> int: int The user ID if found. Otherwise, -1. """ - match = re.search(r"\bUser ID: (\d{17,21})\b", text) + match = TOPIC_REGEX.search(text) if match is not None: return int(match.group(1)) return -1 @@ -276,3 +280,19 @@ async def wrapper(self, ctx: commands.Context, *args, **kwargs): def escape_code_block(text): return re.sub(r"```", "`\u200b``", text) + + +def format_channel_name(author, guild, exclude_channel=None): + """Sanitises a username for use with text channel names""" + name = author.name.lower() + name = new_name = ( + "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" + ) + f"-{author.discriminator}" + + counter = 1 + existed = set(c.name for c in guild.text_channels if c != exclude_channel) + while new_name in existed: + new_name = f"{name}_{counter}" # multiple channels with same name + counter += 1 + + return new_name From 55b25cb1eb0e8b48c7f766b7bae7be80c2d2b1f1 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 4 Dec 2019 02:41:48 -0800 Subject: [PATCH 26/38] ... --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f253d9a9eb..d31511520e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Multi-command alias is now more stable. With support for a single quote escape `\"`. - New command `?freply`, which behaves exactly like `?reply` with the addition that you can substitute `{channel}`, `{recipient}`, and `{author}` to be their respective values. - New command `?repair`, repair any broken Modmail thread (with help from @officialpiyush). -- Recipients gets a feedback when they edit message. +- Recipient get feedback when they edit message. ### Changed From e9430d06d5ca64b7d17b84771a6f92643209809f Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 4 Dec 2019 03:05:03 -0800 Subject: [PATCH 27/38] Swapped contact position, fixed a slight issue with audit from last update --- CHANGELOG.md | 3 ++- bot.py | 2 +- cogs/modmail.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d31511520e..8aa9f11640 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.2-dev5 +# v3.3.2-dev6 (Development update, very likely to be unstable!) @@ -30,6 +30,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - The look of alias and snippet when previewing. - Message ID of the thread embed is saved in DB, instead of the original message. +- Swapped the position of user and category for `?contact`. ### Fixed diff --git a/bot.py b/bot.py index 2c981d26a8..a3c60c46c6 100644 --- a/bot.py +++ b/bot.py @@ -986,7 +986,7 @@ async def on_guild_channel_delete(self, channel): mod = entry.user except AttributeError as e: # discord.py broken implementation with discord API - logger.warning("Failed to retrieve audit log.", str(e)) + logger.warning("Failed to retrieve audit log: %s.", str(e)) return if mod == self.user: diff --git a/cogs/modmail.py b/cogs/modmail.py index 25bb0366b8..52c633efcb 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -878,9 +878,9 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): async def contact( self, ctx, - category: Optional[discord.CategoryChannel] = None, - *, user: Union[discord.Member, discord.User], + *, + category: discord.CategoryChannel = None ): """ Create a thread with a specified member. From 1d2f727f8ff1b292d359c3cf0e2b18513a6a38f2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 4 Dec 2019 03:05:42 -0800 Subject: [PATCH 28/38] whoops --- .github/workflows/build_docker.yml | 28 ++++++++++++++++++++++++++++ cogs/modmail.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build_docker.yml diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml new file mode 100644 index 0000000000..176dbc6ad4 --- /dev/null +++ b/.github/workflows/build_docker.yml @@ -0,0 +1,28 @@ +name: Build Docker to GPR and Release + +on: release: + types: [published] + +jobs: + build-docker-image: + runs-on: ubuntu-latest + steps: + - name: Pull source + uses: actions/checkout@v1 + + - name: Build Docker image + uses: actions/docker/cli@master + with: + args: build . -t modmail:{{ GITHUB_REF }} + + - name: Save the image + uses: actions/docker/cli@master + with: + args: save my-image:latest + + - name: Upload to release + uses: JasonEtco/upload-to-release@master + with: + args: my-image.tar + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/cogs/modmail.py b/cogs/modmail.py index 52c633efcb..38ab1b8a71 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -880,7 +880,7 @@ async def contact( ctx, user: Union[discord.Member, discord.User], *, - category: discord.CategoryChannel = None + category: discord.CategoryChannel = None, ): """ Create a thread with a specified member. From e6e1a514505ba10f9ab7c4d84d9210eec2a2bda2 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Wed, 4 Dec 2019 03:06:25 -0800 Subject: [PATCH 29/38] Delete build_docker.yml --- .github/workflows/build_docker.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/build_docker.yml diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml deleted file mode 100644 index 176dbc6ad4..0000000000 --- a/.github/workflows/build_docker.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Build Docker to GPR and Release - -on: release: - types: [published] - -jobs: - build-docker-image: - runs-on: ubuntu-latest - steps: - - name: Pull source - uses: actions/checkout@v1 - - - name: Build Docker image - uses: actions/docker/cli@master - with: - args: build . -t modmail:{{ GITHUB_REF }} - - - name: Save the image - uses: actions/docker/cli@master - with: - args: save my-image:latest - - - name: Upload to release - uses: JasonEtco/upload-to-release@master - with: - args: my-image.tar - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 1ded678c1c669d02a543edcec5f36103886274f8 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 4 Dec 2019 03:07:38 -0800 Subject: [PATCH 30/38] whoops --- .github/workflows/build_docker.yml | 28 ---------------------------- .gitignore | 3 ++- 2 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 .github/workflows/build_docker.yml diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml deleted file mode 100644 index 176dbc6ad4..0000000000 --- a/.github/workflows/build_docker.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Build Docker to GPR and Release - -on: release: - types: [published] - -jobs: - build-docker-image: - runs-on: ubuntu-latest - steps: - - name: Pull source - uses: actions/checkout@v1 - - - name: Build Docker image - uses: actions/docker/cli@master - with: - args: build . -t modmail:{{ GITHUB_REF }} - - - name: Save the image - uses: actions/docker/cli@master - with: - args: save my-image:latest - - - name: Upload to release - uses: JasonEtco/upload-to-release@master - with: - args: my-image.tar - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 79f95c8388..463016ca95 100644 --- a/.gitignore +++ b/.gitignore @@ -135,4 +135,5 @@ plugins/ !plugins/registry.json temp/ test.py -stack.yml \ No newline at end of file +stack.yml +.github/workflows/build_docker.yml \ No newline at end of file From 72f8962cfde6d66bd3272265920f035c00310ba4 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:55:00 -0800 Subject: [PATCH 31/38] Fix some stuff --- CHANGELOG.md | 2 ++ cogs/modmail.py | 35 ++++++++++++++++++----------------- cogs/utility.py | 47 +++++++++++++++++++++++++++++++---------------- core/models.py | 3 ++- core/utils.py | 5 +++++ 5 files changed, 58 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa9f11640..ba5b654a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - The look of alias and snippet when previewing. - Message ID of the thread embed is saved in DB, instead of the original message. - Swapped the position of user and category for `?contact`. +- The log file will no longer grow infinitely large. +- Hard limit of maximum 25 steps for alias. ### Fixed diff --git a/cogs/modmail.py b/cogs/modmail.py index 38ab1b8a71..6881ac04d4 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -17,16 +17,7 @@ from core.paginator import EmbedPaginatorSession from core.thread import Thread from core.time import UserFriendlyTime, human_timedelta -from core.utils import ( - format_preview, - User, - create_not_found_embed, - format_description, - trigger_typing, - escape_code_block, - match_user_id, - format_channel_name, -) +from core.utils import * logger = getLogger(__name__) @@ -161,15 +152,18 @@ async def snippet(self, ctx, *, name: str.lower = None): if val is None: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") else: - embed = discord.Embed(color=self.bot.main_color) - embed.add_field(name=f"`{name}` will send:", value=val) + embed = discord.Embed( + title=f'Snippet - "{name}":', + description=val, + color=self.bot.main_color + ) return await ctx.send(embed=embed) if not self.bot.snippets: embed = discord.Embed( color=self.bot.error_color, description="You dont have any snippets at the moment." ) - embed.set_footer(text=f'Check "{self.bot.prefix}help snippet" to add a snippet.') + embed.set_footer(text=f'Check "{self.bot.prefix}help snippet add" to add a snippet.') embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) return await ctx.send(embed=embed) @@ -194,9 +188,11 @@ async def snippet_raw(self, ctx, *, name: str.lower): if val is None: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") else: - embed = discord.Embed(color=self.bot.main_color) - val = escape_code_block(val) - embed.add_field(name=f"`{name}` will send:", value=f"```\n{val}```") + val = truncate(escape_code_block(val), 2048 - 7) + embed = discord.Embed(title=f'Raw snippet - "{name}":', + description=f"```\n{val}```", + color=self.bot.main_color) + return await ctx.send(embed=embed) @snippet.command(name="add") @@ -205,6 +201,11 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte """ Add a snippet. + Simply to add a snippet, do: ``` + {prefix}snippet add hey hello there :) + ``` + then when you type `{prefix}hey`, "hello there :)" will get sent to the recipient. + To add a multi-word snippet name, use quotes: ``` {prefix}snippet add "two word" this is a two word snippet. ``` @@ -1245,7 +1246,7 @@ async def repair(self, ctx): ) ) if len(users) == 1: - user = users[0] + user = users.pop() name = format_channel_name( user, self.bot.modmail_guild, exclude_channel=ctx.channel ) diff --git a/cogs/utility.py b/cogs/utility.py index 4282ed22a8..3b624a9ad4 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -916,23 +916,30 @@ async def alias(self, ctx, *, name: str.lower = None): description=f"Alias `{name}` is invalid, this alias will now be deleted." "This alias will now be deleted.", ) - embed.add_field(name=f"{name}` used to be:", value=val) + embed.add_field(name=f"{name}` used to be:", value=utils.truncate(val, 1024)) self.bot.aliases.pop(name) await self.bot.config.update() return await ctx.send(embed=embed) if len(values) == 1: - embed = discord.Embed(color=self.bot.main_color) - embed.add_field(name=f"`{name}` points to:", value=values[0]) - else: embed = discord.Embed( - color=self.bot.main_color, - description=f"**`{name}` points to the following steps:**", + title=f'Alias - "{name}":', + description=values[0], + color=self.bot.main_color ) - for i, val in enumerate(values, start=1): - embed.add_field(name=f"Step {i}:", value=val) + return await ctx.send(embed=embed) - return await ctx.send(embed=embed) + else: + embeds = [] + for i, val in enumerate(values, start=1): + embed = discord.Embed( + color=self.bot.main_color, + title=f'Alias - "{name}" - Step {i}:', + description=val + ) + embeds += [embed] + session = EmbedPaginatorSession(ctx, *embeds) + return await session.run() if not self.bot.aliases: embed = discord.Embed( @@ -964,15 +971,15 @@ async def alias_raw(self, ctx, *, name: str.lower): embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias") return await ctx.send(embed=embed) - embed = discord.Embed(color=self.bot.main_color) - val = utils.escape_code_block(val) - embed.add_field(name=f"`{name}` points to:", value=f"```\n{val}```") + val = utils.truncate(utils.escape_code_block(val), 2048-7) + embed = discord.Embed(title=f'Raw alias - "{name}":', + description=f"```\n{val}```", + color=self.bot.main_color) + return await ctx.send(embed=embed) async def make_alias(self, name, value, action): values = utils.parse_alias(value) - save_aliases = [] - if not values: embed = discord.Embed( title="Error", @@ -982,12 +989,20 @@ async def make_alias(self, name, value, action): embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') return embed + if len(values) > 25: + embed = discord.Embed(title="Error", + description="Too many steps, max=25.", + color=self.bot.error_color) + return embed + + save_aliases = [] + multiple_alias = len(values) > 1 embed = discord.Embed(title=f"{action} alias", color=self.bot.main_color) if not multiple_alias: - embed.add_field(name=f"`{name}` points to:", value=values[0]) + embed.add_field(name=f"`{name}` points to:", value=utils.truncate(values[0], 1024)) else: embed.description = f"`{name}` now points to the following steps:" @@ -1018,7 +1033,7 @@ async def make_alias(self, name, value, action): else: save_aliases.append(val) if multiple_alias: - embed.add_field(name=f"Step {i}:", value=val) + embed.add_field(name=f"Step {i}:", value=utils.truncate(val, 1024)) self.bot.aliases[name] = " && ".join(f'"{a}"' for a in save_aliases) await self.bot.config.update() diff --git a/core/models.py b/core/models.py index f55526f573..e19086b198 100644 --- a/core/models.py +++ b/core/models.py @@ -2,6 +2,7 @@ import re import sys from enum import IntEnum +from logging.handlers import RotatingFileHandler from string import Formatter import discord @@ -120,7 +121,7 @@ def format(self, record): def configure_logging(name, level=None): global ch_debug, log_level - ch_debug = logging.FileHandler(name, mode="a+") + ch_debug = RotatingFileHandler(name, mode="a+", maxBytes=48000, backupCount=1) formatter_debug = FileFormatter( "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", diff --git a/core/utils.py b/core/utils.py index a3b38d63c8..216955d050 100644 --- a/core/utils.py +++ b/core/utils.py @@ -11,6 +11,11 @@ import discord from discord.ext import commands +__all__ = ['strtobool', 'User', 'truncate', 'format_preview', 'is_image_url', + 'parse_image_url', 'human_join', 'days', 'cleanup_code', 'match_user_id', + 'create_not_found_embed', 'parse_alias', 'normalize_alias', 'format_description', 'trigger_typing', + 'escape_code_block', 'format_channel_name'] + def strtobool(val): if isinstance(val, bool): From 6d15d8ff5cb1e271433e66a795ddb8e2ecbed36b Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 9 Dec 2019 01:10:39 -0800 Subject: [PATCH 32/38] Bump version to 3.4.0-dev6 --- CHANGELOG.md | 2 +- bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0967bb4268..88421c0b20 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.2-dev6 +# v3.4.0-dev6 (Development update, very likely to be unstable!) diff --git a/bot.py b/bot.py index a3c60c46c6..a2e1a0bf4a 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.2-dev5" +__version__ = "3.4.0-dev6" import asyncio From 84a7910690ea57deb7c9b93ddbbe6b6c16376852 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 9 Dec 2019 01:34:43 -0800 Subject: [PATCH 33/38] Changed disable to disable new --- CHANGELOG.md | 1 + cogs/modmail.py | 25 ++++++++++++++++++------- cogs/utility.py | 20 +++++++++----------- core/utils.py | 23 +++++++++++++++++++---- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88421c0b20..55b897ad1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Swapped the position of user and category for `?contact`. - The log file will no longer grow infinitely large. - Hard limit of maximum 25 steps for alias. +- `?disable` is now `?disable new`. ### Fixed diff --git a/cogs/modmail.py b/cogs/modmail.py index 6881ac04d4..af75e39697 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -153,9 +153,7 @@ async def snippet(self, ctx, *, name: str.lower = None): embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") else: embed = discord.Embed( - title=f'Snippet - "{name}":', - description=val, - color=self.bot.main_color + title=f'Snippet - "{name}":', description=val, color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -189,9 +187,11 @@ async def snippet_raw(self, ctx, *, name: str.lower): embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") else: val = truncate(escape_code_block(val), 2048 - 7) - embed = discord.Embed(title=f'Raw snippet - "{name}":', - description=f"```\n{val}```", - color=self.bot.main_color) + embed = discord.Embed( + title=f'Raw snippet - "{name}":', + description=f"```\n{val}```", + color=self.bot.main_color, + ) return await ctx.send(embed=embed) @@ -1309,11 +1309,22 @@ async def enable(self, ctx): @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def disable(self, ctx): + """ + Disable partial or full Modmail thread functions. + + To stop all new threads from being created, do `{prefix}disable new`. + To stop all existing threads from DMing Modmail, do `{prefix}disable all`. + To check if the DM function for Modmail is enabled, do `{prefix}isenable`. + """ + await ctx.send_help(ctx.command) + + @disable.command(name="new") + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + async def disable_new(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", diff --git a/cogs/utility.py b/cogs/utility.py index 3b624a9ad4..e042fbf666 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -923,9 +923,7 @@ async def alias(self, ctx, *, name: str.lower = None): if len(values) == 1: embed = discord.Embed( - title=f'Alias - "{name}":', - description=values[0], - color=self.bot.main_color + title=f'Alias - "{name}":', description=values[0], color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -935,7 +933,7 @@ async def alias(self, ctx, *, name: str.lower = None): embed = discord.Embed( color=self.bot.main_color, title=f'Alias - "{name}" - Step {i}:', - description=val + description=val, ) embeds += [embed] session = EmbedPaginatorSession(ctx, *embeds) @@ -971,10 +969,10 @@ async def alias_raw(self, ctx, *, name: str.lower): embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias") return await ctx.send(embed=embed) - val = utils.truncate(utils.escape_code_block(val), 2048-7) - embed = discord.Embed(title=f'Raw alias - "{name}":', - description=f"```\n{val}```", - color=self.bot.main_color) + val = utils.truncate(utils.escape_code_block(val), 2048 - 7) + embed = discord.Embed( + title=f'Raw alias - "{name}":', description=f"```\n{val}```", color=self.bot.main_color + ) return await ctx.send(embed=embed) @@ -990,9 +988,9 @@ async def make_alias(self, name, value, action): return embed if len(values) > 25: - embed = discord.Embed(title="Error", - description="Too many steps, max=25.", - color=self.bot.error_color) + embed = discord.Embed( + title="Error", description="Too many steps, max=25.", color=self.bot.error_color + ) return embed save_aliases = [] diff --git a/core/utils.py b/core/utils.py index 216955d050..a4f14182a5 100644 --- a/core/utils.py +++ b/core/utils.py @@ -11,10 +11,25 @@ import discord from discord.ext import commands -__all__ = ['strtobool', 'User', 'truncate', 'format_preview', 'is_image_url', - 'parse_image_url', 'human_join', 'days', 'cleanup_code', 'match_user_id', - 'create_not_found_embed', 'parse_alias', 'normalize_alias', 'format_description', 'trigger_typing', - 'escape_code_block', 'format_channel_name'] +__all__ = [ + "strtobool", + "User", + "truncate", + "format_preview", + "is_image_url", + "parse_image_url", + "human_join", + "days", + "cleanup_code", + "match_user_id", + "create_not_found_embed", + "parse_alias", + "normalize_alias", + "format_description", + "trigger_typing", + "escape_code_block", + "format_channel_name", +] def strtobool(val): From 52ec750f1a671cf00d7c6dedcb5a1f4b1f1b0f95 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 12 Dec 2019 23:33:22 -0800 Subject: [PATCH 34/38] Add poetry support --- poetry.lock | 474 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 39 +++- 2 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000000..568f53fa6a --- /dev/null +++ b/poetry.lock @@ -0,0 +1,474 @@ +[[package]] +category = "main" +description = "Async http client/server framework (asyncio)" +name = "aiohttp" +optional = false +python-versions = ">=3.5.3" +version = "3.5.4" + +[package.dependencies] +async-timeout = ">=3.0,<4.0" +attrs = ">=17.3.0" +chardet = ">=2.0,<4.0" +multidict = ">=4.0,<5.0" +yarl = ">=1.0,<2.0" + +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.3" + +[[package]] +category = "dev" +description = "An abstract syntax tree for Python with inference support." +name = "astroid" +optional = false +python-versions = ">=3.5.*" +version = "2.3.3" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0,<1.5.0" +six = ">=1.12,<2.0" +wrapt = ">=1.11.0,<1.12.0" + +[package.dependencies.typed-ast] +python = "<3.8" +version = ">=1.4.0,<1.5" + +[[package]] +category = "main" +description = "Timeout context manager for asyncio programs" +name = "async-timeout" +optional = false +python-versions = ">=3.5.3" +version = "3.0.1" + +[[package]] +category = "main" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[[package]] +category = "dev" +description = "Security oriented static analyser for python code." +name = "bandit" +optional = false +python-versions = "*" +version = "1.6.2" + +[package.dependencies] +GitPython = ">=1.0.1" +PyYAML = ">=3.13" +colorama = ">=0.3.9" +six = ">=1.10.0" +stevedore = ">=1.20.0" + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.3b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +toml = ">=0.9.4" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" + +[[package]] +category = "main" +description = "Cross-platform colored terminal text." +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "main" +description = "A python wrapper for the Discord API" +name = "discord.py" +optional = false +python-versions = ">=3.5.3" +version = "1.2.5" + +[package.dependencies] +aiohttp = ">=3.3.0,<3.6.0" +websockets = ">=6.0,<7.0" + +[[package]] +category = "main" +description = "DNS toolkit" +name = "dnspython" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.16.0" + +[[package]] +category = "main" +description = "Emoji for Python" +name = "emoji" +optional = false +python-versions = "*" +version = "0.5.4" + +[[package]] +category = "dev" +description = "Discover and load entry points from installed packages." +name = "entrypoints" +optional = false +python-versions = ">=2.7" +version = "0.3" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8, pyflakes and co" +name = "flake8" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.7.9" + +[package.dependencies] +entrypoints = ">=0.3.0,<0.4.0" +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.5.0,<2.6.0" +pyflakes = ">=2.1.0,<2.2.0" + +[[package]] +category = "main" +description = "Backport of the concurrent.futures package from Python 3.2" +name = "futures" +optional = true +python-versions = "*" +version = "3.1.1" + +[[package]] +category = "dev" +description = "Git Object Database" +name = "gitdb2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.0.6" + +[package.dependencies] +smmap2 = ">=2.0.0" + +[[package]] +category = "dev" +description = "Python Git Library" +name = "gitpython" +optional = false +python-versions = ">=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.0.5" + +[package.dependencies] +gitdb2 = ">=2.0.0" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8" + +[[package]] +category = "main" +description = "An ISO 8601 date/time/duration parser and formatter" +name = "isodate" +optional = false +python-versions = "*" +version = "0.6.0" + +[package.dependencies] +six = "*" + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[[package]] +category = "dev" +description = "A fast and thorough lazy object proxy." +name = "lazy-object-proxy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.3" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "main" +description = "Non-blocking MongoDB driver for Tornado or asyncio" +name = "motor" +optional = true +python-versions = "*" +version = "2.1.0" + +[package.dependencies] +futures = "*" +pymongo = ">=3.10,<4" + +[[package]] +category = "main" +description = "multidict implementation" +name = "multidict" +optional = false +python-versions = ">=3.5" +version = "4.7.1" + +[[package]] +category = "main" +description = "Convert data to their natural (human-readable) format" +name = "natural" +optional = false +python-versions = "*" +version = "0.2.0" + +[package.dependencies] +six = "*" + +[[package]] +category = "main" +description = "Parse human-readable date/time text." +name = "parsedatetime" +optional = false +python-versions = "*" +version = "2.5" + +[[package]] +category = "dev" +description = "Python Build Reasonableness" +name = "pbr" +optional = false +python-versions = "*" +version = "5.4.4" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.5.0" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.1.1" + +[[package]] +category = "dev" +description = "python code static checker" +name = "pylint" +optional = false +python-versions = ">=3.5.*" +version = "2.4.4" + +[package.dependencies] +astroid = ">=2.3.0,<2.4" +colorama = "*" +isort = ">=4.2.5,<5" +mccabe = ">=0.6,<0.7" + +[[package]] +category = "main" +description = "Python driver for MongoDB " +name = "pymongo" +optional = true +python-versions = "*" +version = "3.10.0" + +[[package]] +category = "main" +description = "Extensions to the standard Python datetime module" +name = "python-dateutil" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +version = "2.8.1" + +[package.dependencies] +six = ">=1.5" + +[[package]] +category = "main" +description = "Add .env support to your django/flask apps in development and deployments" +name = "python-dotenv" +optional = false +python-versions = "*" +version = "0.10.3" + +[[package]] +category = "dev" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.2" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.13.0" + +[[package]] +category = "dev" +description = "A pure Python implementation of a sliding window memory map manager" +name = "smmap2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.0.5" + +[[package]] +category = "dev" +description = "Manage dynamic plugins for Python applications" +name = "stevedore" +optional = false +python-versions = "*" +version = "1.31.0" + +[package.dependencies] +pbr = ">=2.0.0,<2.1.0 || >2.1.0" +six = ">=1.10.0" + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.0" + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +marker = "implementation_name == \"cpython\" and python_version < \"3.8\"" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.0" + +[[package]] +category = "main" +description = "Fast implementation of asyncio event loop on top of libuv" +name = "uvloop" +optional = false +python-versions = "*" +version = "0.14.0" + +[[package]] +category = "main" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +name = "websockets" +optional = false +python-versions = ">=3.4" +version = "6.0" + +[[package]] +category = "dev" +description = "Module for decorators, wrappers and monkey patching." +name = "wrapt" +optional = false +python-versions = "*" +version = "1.11.2" + +[[package]] +category = "main" +description = "Yet another URL library" +name = "yarl" +optional = false +python-versions = ">=3.5" +version = "1.4.2" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +content-hash = "5d9df5468292faf3132d69c597109a1eac557695985d46a78a1edca3764f918e" +python-versions = "^3.7" + +[metadata.hashes] +aiohttp = ["00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", "0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", "09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", "199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", "296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", "368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", "40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", "629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", "6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", "87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", "9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", "9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", "9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", "a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", "a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", "a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", "acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", "b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", "c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", "cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", "d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", "e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"] +appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] +astroid = ["71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", "840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"] +async-timeout = ["0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"] +attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] +bandit = ["336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", "41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"] +black = ["09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", "68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"] +chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] +click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] +colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"] +"discord.py" = ["7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d"] +dnspython = ["36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", "f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"] +emoji = ["60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174"] +entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] +flake8 = ["45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", "49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"] +futures = ["3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b", "51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"] +gitdb2 = ["1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", "96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"] +gitpython = ["9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", "c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245"] +idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] +isodate = ["2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", "aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"] +isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"] +lazy-object-proxy = ["0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", "194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", "1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", "4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", "48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", "5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", "59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", "8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", "9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", "9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", "97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", "9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", "a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", "a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", "ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", "cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", "d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", "d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", "eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", "efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"] +mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] +motor = ["599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909", "756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4", "97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a"] +multidict = ["09c19f642e055550c9319d5123221b7e07fc79bda58122aa93910e52f2ab2f29", "0c1a5d5f7aa7189f7b83c4411c2af8f1d38d69c4360d5de3eea129c65d8d7ce2", "12f22980e7ed0972a969520fb1e55682c9fca89a68b21b49ec43132e680be812", "258660e9d6b52de1a75097944e12718d3aa59adc611b703361e3577d69167aaf", "3374a23e707848f27b3438500db0c69eca82929337656fce556bd70031fbda74", "503b7fce0054c73aa631cc910a470052df33d599f3401f3b77e54d31182525d5", "6ce55f2c45ffc90239aab625bb1b4864eef33f73ea88487ef968291fbf09fb3f", "725496dde5730f4ad0a627e1a58e2620c1bde0ad1c8080aae15d583eb23344ce", "a3721078beff247d0cd4fb19d915c2c25f90907cf8d6cd49d0413a24915577c6", "ba566518550f81daca649eded8b5c7dd09210a854637c82351410aa15c49324a", "c42362750a51a15dc905cb891658f822ee5021bfbea898c03aa1ed833e2248a5", "cf14aaf2ab067ca10bca0b14d5cbd751dd249e65d371734bc0e47ddd8fafc175", "cf24e15986762f0e75a622eb19cfe39a042e952b8afba3e7408835b9af2be4fb", "d7b6da08538302c5245cd3103f333655ba7f274915f1f5121c4f4b5fbdb3febe", "e27e13b9ff0a914a6b8fb7e4947d4ac6be8e4f61ede17edffabd088817df9e26", "e53b205f8afd76fc6c942ef39e8ee7c519c775d336291d32874082a87802c67c", "ec804fc5f68695d91c24d716020278fcffd50890492690a7e1fef2e741f7172c"] +natural = ["18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2"] +parsedatetime = ["3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", "d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"] +pbr = ["139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", "61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"] +pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] +pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] +pylint = ["3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", "886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"] +pymongo = ["0369136c6e79c5edc16aa5de2b48a1b1c1fe5e6f7fc5915a2deaa98bd6e9dad5", "08364e1bea1507c516b18b826ec790cb90433aec2f235033ec5eecfd1011633b", "0af1d2bc8cc9503bf92ec3669a77ec3a6d7938193b583fb867b7e9696eed52e8", "0cfd1aeeb8c0a634646ab3ebeb4ce6828b94b2e33553a69ff7e6c07c250bf201", "15bbd2b5397f7d22498e2f2769fd698a8a247b9cc1a630ee8dabf647fb333480", "1b4a13dff15641e58620524db15d7a323d60572b2b187261c5cb58c36d74778d", "22fbdb908257f9aaaa372a7684f3e094a05ca52eb84f8f381c8b1827c49556fd", "264272fd1c95fc48002ad85d5e41270831777b4180f2500943e45e12b2a3ab43", "3372e98eebbfd05ebf020388003f8a4438bed41e0fef1ef696d2c13633c416c8", "339d24ecdc42745d2dc09b26fda8151988e806ca81134a7bd10513c4031d91e1", "38281855fc3961ba5510fbb503b8d16cc1fcb326e9f7ba0dd096ed4eb72a7084", "4acdd2e16392472bfd49ca49038845c95e5254b5af862b55f7f2cc79aa258886", "4e0c006bc6e98e861b678432e05bf64ba3eb889b6ab7e7bf1ebaecf9f1ba0e58", "4e4284bcbe4b7be1b37f9641509085b715c478e7fbf8f820358362b5dd359379", "4e5e94a5f9823f0bd0c56012a57650bc6772636c29d83d253260c26b908fcfd9", "4e61f30800a40f1770b2ec56bbf5dc0f0e3f7e9250eb05fa4feb9ccb7bbe39ca", "53577cf57ba9d93b58ab41d45250277828ff83c5286dde14f855e4b17ec19976", "681cb31e8631882804a6cc3c8cc8f54a74ff3a82261a78e50f20c5eec05ac855", "6dfc2710f43dd1d66991a0f160d196356732ccc8aa9dbc6875aeba78388fa142", "72218201b13d8169be5736417987e9a0a3b10d4349e40e4db7a6a5ac670c7ef2", "7247fbcdbf7ab574eb70743461b3cfc14d9cfae3f27a9afb6ce14d87f67dd0b5", "72651f4b4adf50201891580506c8cca465d94d38f26ed92abfc56440662c723c", "87b3aaf12ad6a9b5570b12d2a4b8802757cb3588a903aafd3c25f07f9caf07e3", "87c28b7b37617c5a01eb396487f7d3b61a453e1fa0475a175ab87712d6f5d52f", "88efe627b628f36ef53f09abb218d4630f83d8ebde7028689439559475c43dae", "89bfbca22266f12df7fb80092b7c876734751d02b93789580b68957ad4a8bf56", "908a3caf348a672b28b8a06fe7b4a27c2fdcf7f873df671e4027d48bcd7f971f", "9128e7bea85f3a3041306fa14a7aa82a24b47881918500e1b8396dd1c933b5a6", "9737d6d688a15b8d5c0bfa909638b79261e195be817b9f1be79c722bbb23cd76", "98a8305da158f46e99e7e51db49a2f8b5fcdd7683ea7083988ccb9c4450507a6", "99285cd44c756f0900cbdb5fe75f567c0a76a273b7e0467f23cb76f47e60aac0", "9ed568f8026ffeb00ce31e5351e0d09d704cc19a29549ba4da0ac145d2a26fdf", "a006162035032021dfd00a879643dc06863dac275f9210d843278566c719eebc", "a03cb336bc8d25a11ff33b94967478a9775b0d2b23b39e952d9cc6cb93b75d69", "a863ceb67be163060d1099b7e89b6dd83d6dd50077c7ceae31ac844c4c2baff9", "b82628eaf0a16c1f50e1c205fd1dd406d7874037dd84643da89e91b5043b5e82", "bc6446a41fb7eeaf2c808bab961b9bac81db0f5de69eab74eebe1b8b072399f7", "c42d290ed54096355838421cf9d2a56e150cb533304d2439ef1adf612a986eaf", "c43879fe427ea6aa6e84dae9fbdc5aa14428a4cfe613fe0fee2cc004bf3f307c", "c566cbdd1863ba3ccf838656a1403c3c81fdb57cbe3fdd3515be7c9616763d33", "c5b7a0d7e6ca986de32b269b6dbbd5162c1a776ece72936f55decb4d1b197ee9", "ca109fe9f74da4930590bb589eb8fdf80e5d19f5cd9f337815cac9309bbd0a76", "d0260ba68f9bafd8775b2988b5aeace6e69a37593ec256e23e150c808160c05c", "d12d86e771fc3072a0e6bdbf4e417c63fec85ee47cb052ba7ad239403bf5e154", "d2ce33501149b373118fcfec88a292a87ef0b333fb30c7c6aac72fe64700bdf6", "d582ea8496e2a0e124e927a67dca55c8833f0dbfbc2c84aaf0e5949a2dd30c51", "d68b9ab0a900582a345fb279675b0ad4fac07d6a8c2678f12910d55083b7240d", "dbf1fa571db6006907aeaf6473580aaa76041f4f3cd1ff8a0039fd0f40b83f6d", "e032437a7d2b89dab880c79379d88059cee8019da0ff475d924c4ccab52db88f", "e0f5798f3ad60695465a093e3d002f609c41fef3dcb97fcefae355d24d3274cf", "e756355704a2cf91a7f4a649aa0bbf3bbd263018b9ed08f60198c262f4ee24b6", "e824b4b87bd88cbeb25c8babeadbbaaaf06f02bbb95a93462b7c6193a064974e", "ea1171470b52487152ed8bf27713cc2480dc8b0cd58e282a1bff742541efbfb8", "fa19aef44d5ed8f798a8136ff981aedfa508edac3b1bed481eca5dde5f14fd3d", "faf83d20c041637cb277e5fdb59abc217c40ab3202dd87cc95d6fbd9ce5ffd9b", "fceb6ae5a149a42766efb8344b0df6cfb21b55c55f360170abaddb11d43af0f1"] +python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"] +python-dotenv = ["debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", "f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544"] +pyyaml = ["0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", "2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", "35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", "38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", "483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", "4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", "7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", "8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", "e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", "ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"] +six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"] +smmap2 = ["0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", "29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"] +stevedore = ["01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", "e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14"] +toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] +typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] +uvloop = ["08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", "123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", "4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", "4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", "afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", "b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", "bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", "e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", "f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"] +websockets = ["0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", "2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", "5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", "5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", "669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", "695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", "6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", "79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", "7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", "82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", "8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", "91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", "952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", "99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", "9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", "a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", "cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", "e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", "e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", "ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", "f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"] +wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"] +yarl = ["0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", "0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", "2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", "25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", "26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", "308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", "3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", "58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", "5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", "6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", "944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", "a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", "a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", "c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", "c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", "d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", "e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"] diff --git a/pyproject.toml b/pyproject.toml index 420b418e6f..d42e975573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,4 +17,41 @@ exclude = ''' | temp )/ ) -''' \ No newline at end of file +''' + +[tool.poetry] +name = 'Modmail' +version = '3.4.0-dev6' +description = 'Modmail is similar to Reddits Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way.' +license = 'AGPL-3.0-only' +authors = [ + 'kyb3r ', + '4jr ', + 'Taki ' +] +readme = 'README.md' +repository = 'https://github.com/kyb3r/modmail' +homepage = 'https://github.com/kyb3r/modmail' +keywords = ['discord', 'modmail'] + +[tool.poetry.dependencies] +python = "^3.7" +"discord.py" = "=1.2.5" +uvloop = "^0.14.0" +python-dotenv = "^0.10.3" +parsedatetime = "^2.5" +dnspython = "^1.16" +isodate = "^0.6.0" +natural = "^0.2.0" +motor = {version = "^2.1", optional = true} +emoji = "^0.5.4" +python-dateutil = "^2.8" +colorama = "^0.4.3" +aiohttp = "<3.6.0,>=3.3.0" + +[tool.poetry.dev-dependencies] +black = {version = "=19.3b0", allows-prereleases = true} +pylint = "^2.4" +bandit = "^1.6" +flake8 = "^3.7" + From f6044de44fc92d8db7ad61e7668c3f37b5c06b56 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Dec 2019 02:47:42 -0800 Subject: [PATCH 35/38] fixed reaction / delete --- CHANGELOG.md | 6 ++- Pipfile | 2 +- README.md | 2 +- bot.py | 104 +++++++++++++++++++++++++++++----------------- cogs/modmail.py | 34 +++++---------- cogs/utility.py | 6 +-- core/paginator.py | 7 +--- core/thread.py | 75 ++++++++++++++++++++++++--------- pyproject.toml | 2 +- 9 files changed, 145 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b897ad1c..b98bf97952 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.4.0-dev6 +# v3.4.0-dev7 (Development update, very likely to be unstable!) @@ -25,6 +25,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - New command `?freply`, which behaves exactly like `?reply` with the addition that you can substitute `{channel}`, `{recipient}`, and `{author}` to be their respective values. - New command `?repair`, repair any broken Modmail thread (with help from @officialpiyush). - Recipient get feedback when they edit message. +- Chained delete for DMs now comes with a message. +- poetry (in case someone needs it). ### Changed @@ -42,6 +44,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Fixed a lot of issues with `?edit` and `?delete` and recipient message edit. - Masked the error: "AttributeError: 'int' object has no attribute 'name'" - Channel delete event will not be checked until discord.py fixes this issue. +- Chained reaction. +- Chained delete for thread channels. ### Internal diff --git a/Pipfile b/Pipfile index 549ec62653..8f39ad0351 100644 --- a/Pipfile +++ b/Pipfile @@ -18,7 +18,7 @@ motor = ">=2.0.0" natural = "==0.2.0" isodate = ">=0.6.0" dnspython = "~=1.16.0" -parsedatetime = "==2.4" +parsedatetime = "==2.5" aiohttp = "<3.6.0,>=3.3.0" python-dotenv = ">=0.10.3" pipenv = "*" diff --git a/README.md b/README.md index da288a4364..039879c0cf 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
diff --git a/bot.py b/bot.py index a2e1a0bf4a..26d611295c 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.4.0-dev6" +__version__ = "3.4.0-dev7" import asyncio @@ -78,7 +78,7 @@ def __init__(self): "Your MONGO_URI might be copied wrong, try re-copying from the source again. " "Otherwise noted in the following message:" ) - logger.critical(str(e)) + logger.critical(e) sys.exit(0) self.plugin_db = PluginDatabaseClient(self) @@ -506,7 +506,7 @@ async def convert_emoji(self, name: str) -> str: try: name = await converter.convert(ctx, name.strip(":")) except commands.BadArgument as e: - logger.warning("%s is not a valid emoji. %s.", str(e)) + logger.warning("%s is not a valid emoji. %s.", e) raise return name @@ -697,12 +697,14 @@ async def get_thread_cooldown(self, author: discord.Member): return @staticmethod - async def add_reaction(msg, reaction): + async def add_reaction(msg, reaction: discord.Reaction) -> bool: if reaction != "disable": try: await msg.add_reaction(reaction) - except (discord.HTTPException, discord.InvalidArgument): - logger.warning("Failed to add reaction %s.", reaction, exc_info=True) + except (discord.HTTPException, discord.InvalidArgument) as e: + logger.warning("Failed to add reaction %s: %s.", reaction, e) + return False + return True async def process_dm_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" @@ -955,26 +957,43 @@ async def on_raw_reaction_add(self, payload): close_emoji = await self.convert_emoji(self.config["close_emoji"]) if isinstance(channel, discord.DMChannel): - if str(reaction) == str(close_emoji): # closing thread - 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 + thread = await self.threads.find(recipient=user) + if not thread: + return + + if ( + message.embeds + and str(reaction) == str(close_emoji) + and self.config.get("recipient_thread_close") + ): + ts = message.embeds[0].timestamp if thread and ts == thread.channel.created_at: # the reacted message is the corresponding thread creation embed - await thread.close(closer=user) + # closing thread + return await thread.close(closer=user) + if not thread.recipient.dm_channel: + await thread.recipient.create_dm() + try: + linked_message = await thread.find_linked_message_from_dm( + message, either_direction=True + ) + except ValueError as e: + logger.warning("Failed to find linked message for reactions: %s", e) + return else: - if not message.embeds: + thread = await self.threads.find(channel=channel) + if not thread: + return + try: + _, linked_message = await thread.find_linked_messages( + message.id, either_direction=True + ) + except ValueError as e: + logger.warning("Failed to find linked message for reactions: %s", e) return - message_id = str(message.embeds[0].author.url).split("/")[-1] - if message_id.isdigit(): - thread = await self.threads.find(channel=message.channel) - channel = thread.recipient.dm_channel - if not channel: - channel = await thread.recipient.create_dm() - async for msg in channel.history(): - if msg.id == int(message_id): - await msg.add_reaction(reaction) + + if await self.add_reaction(linked_message, reaction): + await self.add_reaction(message, reaction) async def on_guild_channel_delete(self, channel): if channel.guild != self.modmail_guild: @@ -986,7 +1005,8 @@ async def on_guild_channel_delete(self, channel): mod = entry.user except AttributeError as e: # discord.py broken implementation with discord API - logger.warning("Failed to retrieve audit log: %s.", str(e)) + # TODO: waiting for dpy + logger.warning("Failed to retrieve audit log: %s.", e) return if mod == self.user: @@ -1035,18 +1055,20 @@ async def on_member_join(self, member): async def on_message_delete(self, message): """Support for deleting linked messages""" + # TODO: use audit log to check if modmail deleted the message if message.embeds and not isinstance(message.channel, discord.DMChannel): - message_id = str(message.embeds[0].author.url).split("/")[-1] - if message_id.isdigit(): - thread = await self.threads.find(channel=message.channel) - - channel = thread.recipient.dm_channel - - async for msg in channel.history(): - if msg.embeds and msg.embeds[0].author: - url = str(msg.embeds[0].author.url) - if message_id == url.split("/")[-1]: - return await msg.delete() + thread = await self.threads.find(channel=message.channel) + try: + await thread.delete_message(message) + except ValueError as e: + if str(e) not in {"DM message not found.", " Malformed thread message."}: + logger.warning("Failed to find linked message to delete: %s", e) + else: + thread = await self.threads.find(recipient=message.author) + message = await thread.find_linked_message_from_dm(message) + embed = message.embeds[0] + embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url) + await message.edit(embed=embed) async def on_bulk_message_delete(self, messages): await discord.utils.async_all(self.on_message_delete(msg) for msg in messages) @@ -1054,16 +1076,16 @@ async def on_bulk_message_delete(self, messages): async def on_message_edit(self, before, after): if after.author.bot: return + if before.content == after.content: + return + if isinstance(after.channel, discord.DMChannel): thread = await self.threads.find(recipient=before.author) try: await thread.edit_dm_message(after, after.content) except ValueError: _, blocked_emoji = await self.retrieve_emoji() - try: - await after.add_reaction(blocked_emoji) - except (discord.HTTPException, discord.InvalidArgument): - pass + await self.add_reaction(after, blocked_emoji) else: embed = discord.Embed( description="Successfully Edited Message", color=self.main_color @@ -1173,7 +1195,7 @@ async def before_post_metadata(self): self.metadata_loop.cancel() -if __name__ == "__main__": +def main(): try: # noinspection PyUnresolvedReferences import uvloop @@ -1185,3 +1207,7 @@ async def before_post_metadata(self): bot = ModmailBot() bot.run() + + +if __name__ == "__main__": + main() diff --git a/cogs/modmail.py b/cogs/modmail.py index af75e39697..6531e61e70 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -312,10 +312,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = await thread.recipient.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 self.bot.add_reaction(ctx.message, sent_emoji) async def send_scheduled_close_message(self, ctx, after, silent=False): human_delta = human_timedelta(after.dt) @@ -561,10 +558,7 @@ async def nsfw(self, ctx): """Flags a Modmail thread as NSFW (not safe for work).""" await ctx.channel.edit(nsfw=True) sent_emoji, _ = await self.bot.retrieve_emoji() - try: - await ctx.message.add_reaction(sent_emoji) - except (discord.HTTPException, discord.InvalidArgument): - pass + await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -573,10 +567,7 @@ async def sfw(self, ctx): """Flags a Modmail thread as SFW (safe for work).""" await ctx.channel.edit(nsfw=False) sent_emoji, _ = await self.bot.retrieve_emoji() - try: - await ctx.message.add_reaction(sent_emoji) - except (discord.HTTPException, discord.InvalidArgument): - pass + await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -872,7 +863,7 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): ) sent_emoji, _ = await self.bot.retrieve_emoji() - return await ctx.message.add_reaction(sent_emoji) + await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -921,10 +912,7 @@ 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 self.bot.add_reaction(ctx.message, sent_emoji) await asyncio.sleep(3) await ctx.message.delete() @@ -1173,7 +1161,7 @@ async def delete(self, ctx, message_id: int = None): ) sent_emoji, _ = await self.bot.retrieve_emoji() - return await ctx.message.add_reaction(sent_emoji) + await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1188,7 +1176,7 @@ async def repair(self, ctx): if user_id == -1: logger.info("Setting current channel's topic to User ID.") await ctx.channel.edit(topic=f"User ID: {ctx.thread.id}") - return await ctx.message.add_reaction(sent_emoji) + return await self.bot.add_reaction(ctx.message, sent_emoji) logger.info("Attempting to fix a broken thread %s.", ctx.channel.name) @@ -1200,7 +1188,7 @@ async def repair(self, ctx): if thread is not None: logger.debug("Found thread with tempered ID.") await ctx.channel.edit(reason="Fix broken Modmail thread", topic=f"User ID: {user_id}") - return await ctx.message.add_reaction(sent_emoji) + return await self.bot.add_reaction(ctx.message, sent_emoji) # find genesis message to retrieve User ID async for message in ctx.channel.history(limit=10, oldest_first=True): @@ -1229,7 +1217,7 @@ async def repair(self, ctx): await ctx.channel.edit( reason="Fix broken Modmail thread", topic=f"User ID: {user_id}" ) - return await ctx.message.add_reaction(sent_emoji) + return await self.bot.add_reaction(ctx.message, sent_emoji) else: logger.warning("No genesis message found.") @@ -1280,11 +1268,11 @@ async def repair(self, ctx): await ctx.channel.edit( reason="Fix broken Modmail thread", name=name, topic=f"User ID: {user.id}" ) - return await ctx.message.add_reaction(sent_emoji) + return await self.bot.add_reaction(ctx.message, sent_emoji) elif len(users) >= 2: logger.info("Multiple users with the same name and discriminator.") - return await ctx.message.add_reaction(blocked_emoji) + return await self.bot.add_reaction(ctx.message, blocked_emoji) @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) diff --git a/cogs/utility.py b/cogs/utility.py index e042fbf666..f8460c7d52 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -201,7 +201,7 @@ async def send_error_message(self, error): ) return await self.get_destination().send(embed=embed) - logger.warning("CommandNotFound: %s", str(error)) + logger.warning("CommandNotFound: %s", error) embed = discord.Embed(color=self.context.bot.error_color) embed.set_footer(text=f'Command/Category "{command}" not found.') @@ -1723,7 +1723,7 @@ def paginate(text: str): exec(to_compile, env) # pylint: disable=exec-used except Exception as exc: await ctx.send(f"```py\n{exc.__class__.__name__}: {exc}\n```") - return await ctx.message.add_reaction("\u2049") + return await self.bot.add_reaction(ctx.message, "\u2049") func = env["func"] try: @@ -1732,7 +1732,7 @@ def paginate(text: str): except Exception: value = stdout.getvalue() await ctx.send(f"```py\n{value}{traceback.format_exc()}\n```") - return await ctx.message.add_reaction("\u2049") + return await self.bot.add_reaction(ctx.message, "\u2049") else: value = stdout.getvalue() diff --git a/core/paginator.py b/core/paginator.py index 695d194415..7ba1c98b60 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -73,7 +73,7 @@ async def create_base(self, item) -> None: for reaction in self.reaction_map: if len(self.pages) == 2 and reaction in "⏮⏭": continue - await self.base.add_reaction(reaction) + await self.ctx.bot.add_reaction(self.base, reaction) async def _create_base(self, item) -> None: raise NotImplementedError @@ -177,10 +177,7 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: self.running = False sent_emoji, _ = await self.ctx.bot.retrieve_emoji() - try: - await self.ctx.message.add_reaction(sent_emoji) - except (HTTPException, InvalidArgument): - pass + await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) if delete: return await self.base.delete() diff --git a/core/thread.py b/core/thread.py index 236576a281..0926a0acd0 100644 --- a/core/thread.py +++ b/core/thread.py @@ -172,7 +172,7 @@ async def send_recipient_genesis_message(): if recipient_thread_close: close_emoji = self.bot.config["close_emoji"] close_emoji = await self.bot.convert_emoji(close_emoji) - await msg.add_reaction(close_emoji) + await self.bot.add_reaction(msg, close_emoji) await asyncio.gather(send_genesis_message(), send_recipient_genesis_message()) @@ -301,7 +301,7 @@ async def _close( try: self.manager.cache.pop(self.id) except KeyError as e: - logger.error("Thread already closed: %s.", str(e)) + logger.error("Thread already closed: %s.", e) return await self.cancel_closure(all=True) @@ -453,9 +453,16 @@ async def _restart_close_timer(self): ) async def find_linked_messages( - self, message_id: typing.Optional[int] = None + self, + message_id: typing.Optional[int] = None, + either_direction: bool = False, + message1: discord.Message = None, ) -> typing.Tuple[discord.Message, typing.Optional[discord.Message]]: - if message_id is not None: + if message1 is not None: + if not message1.embeds or not message1.embeds[0].author.url: + raise ValueError("Malformed thread message.") + + elif message_id is not None: try: message1 = await self.channel.fetch_message(message_id) except discord.NotFound: @@ -471,7 +478,9 @@ async def find_linked_messages( ].author.name.startswith("Note"): return message1, None - if message1.embeds[0].color.value != self.bot.mod_color: + if message1.embeds[0].color.value != self.bot.mod_color and not ( + either_direction and message1.embeds[0].color.value == self.bot.recipient_color + ): raise ValueError("Thread message not found.") else: async for message1 in self.channel.history(): @@ -479,7 +488,13 @@ async def find_linked_messages( message1.embeds and message1.embeds[0].author.url and message1.embeds[0].color - and message1.embeds[0].color.value == self.bot.mod_color + and ( + message1.embeds[0].color.value == self.bot.mod_color + or ( + either_direction + and message1.embeds[0].color.value == self.bot.recipient_color + ) + ) ): break else: @@ -491,6 +506,10 @@ async def find_linked_messages( raise ValueError("Malformed thread message.") async for msg in self.recipient.history(): + if either_direction: + if msg.id == joint_id: + return message1, msg + if not (msg.embeds and msg.embeds[0].author.url): continue try: @@ -518,35 +537,53 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> await asyncio.gather(*tasks) - async def delete_message(self, message_id: typing.Optional[int]) -> None: + async def delete_message(self, message: typing.Union[int, discord.Message] = None) -> None: try: - message1, message2 = await self.find_linked_messages(message_id) - except ValueError: - logger.warning("Failed to delete message.", exc_info=True) + if isinstance(message, discord.Message): + message1, message2 = await self.find_linked_messages(message1=message) + else: + message1, message2 = await self.find_linked_messages(message) + except ValueError as e: + logger.warning("Failed to delete message: %s.", e) raise - tasks = [message1.delete()] + tasks = [] + if not isinstance(message, discord.Message): + tasks += [message1.delete()] if message2 is not None: tasks += [message2.delete()] - await asyncio.gather(*tasks) + if tasks: + await asyncio.gather(*tasks) + + async def find_linked_message_from_dm(self, message, either_direction=False): + if either_direction and message.embeds: + compare_url = message.embeds[0].author.url + else: + compare_url = None - async def edit_dm_message(self, message: discord.Message, content: str) -> None: async for linked_message in self.channel.history(): if not linked_message.embeds: continue url = linked_message.embeds[0].author.url if not url: continue + if url == compare_url: + return linked_message + msg_id = url.split("#")[-1] try: if int(msg_id) == message.id: - break + return linked_message except ValueError: - logger.warning("Failed to edit message.", exc_info=True) - raise ValueError("Malformed current channel message.") - else: + raise ValueError("Malformed dm channel message.") + raise ValueError("DM channel message not found.") + + async def edit_dm_message(self, message: discord.Message, content: str) -> None: + try: + linked_message = await self.find_linked_message_from_dm(message) + except ValueError: logger.warning("Failed to edit message.", exc_info=True) - raise ValueError("Current channel message not found.") + raise embed = linked_message.embeds[0] embed.add_field(name="**Edited, former message:**", value=embed.description) embed.description = content @@ -784,7 +821,7 @@ 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.", 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) diff --git a/pyproject.toml b/pyproject.toml index d42e975573..9db56ce308 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.4.0-dev6' +version = '3.4.0-dev7' description = 'Modmail is similar to Reddits Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way.' license = 'AGPL-3.0-only' authors = [ From c1dad318bf0a156b494bdd03d55ac11c9ee0df4b Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Dec 2019 03:03:28 -0800 Subject: [PATCH 36/38] I forgot about reaction remove, whoops --- CHANGELOG.md | 2 +- bot.py | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b98bf97952..6606c13c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Fixed a lot of issues with `?edit` and `?delete` and recipient message edit. - Masked the error: "AttributeError: 'int' object has no attribute 'name'" - Channel delete event will not be checked until discord.py fixes this issue. -- Chained reaction. +- Chained reaction add / remove. - Chained delete for thread channels. ### Internal diff --git a/bot.py b/bot.py index 26d611295c..5c25092123 100644 --- a/bot.py +++ b/bot.py @@ -935,7 +935,7 @@ async def on_typing(self, channel, user, _): return await thread.recipient.trigger_typing() - async def on_raw_reaction_add(self, payload): + async def handle_reaction_events(self, payload, *, add): user = self.get_user(payload.user_id) if user.bot: return @@ -960,9 +960,9 @@ async def on_raw_reaction_add(self, payload): thread = await self.threads.find(recipient=user) if not thread: return - if ( - message.embeds + add + and message.embeds and str(reaction) == str(close_emoji) and self.config.get("recipient_thread_close") ): @@ -992,8 +992,21 @@ async def on_raw_reaction_add(self, payload): logger.warning("Failed to find linked message for reactions: %s", e) return - if await self.add_reaction(linked_message, reaction): - await self.add_reaction(message, reaction) + if add: + if await self.add_reaction(linked_message, reaction): + await self.add_reaction(message, reaction) + else: + try: + await linked_message.remove_reaction(reaction, self.user) + await message.remove_reaction(reaction, self.user) + except (discord.HTTPException, discord.InvalidArgument) as e: + logger.warning("Failed to remove reaction: %s", e) + + async def on_raw_reaction_add(self, payload): + await self.handle_reaction_events(payload, add=True) + + async def on_raw_reaction_remove(self, payload): + await self.handle_reaction_events(payload, add=False) async def on_guild_channel_delete(self, channel): if channel.guild != self.modmail_guild: From 232b5543a4f7ecb830ea56670060e729d64ac13e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Dec 2019 03:07:29 -0800 Subject: [PATCH 37/38] Pipenv lock: 1 min, poetry lock: 2 sec, poetry > pipenv --- Pipfile.lock | 240 +++++++++++++++++++++++++++------------------------ 1 file changed, 126 insertions(+), 114 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index adf4fb9de7..7aefd57a71 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e5dd1237af339923e2b40a66f8a1f12682e8b1fb4de0d81f4260a7fd03076dac" + "sha256": "55c46b8ade1feae39241bbef88351ac66f48b22ef077850fe50aec9a5afa18b1" }, "pipfile-spec": 6, "requires": { @@ -60,10 +60,10 @@ }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.9.11" + "version": "==2019.11.28" }, "chardet": { "hashes": [ @@ -74,11 +74,11 @@ }, "colorama": { "hashes": [ - "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", - "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" ], "index": "pypi", - "version": "==0.4.1" + "version": "==0.4.3" }, "discord.py": { "hashes": [ @@ -102,12 +102,6 @@ "index": "pypi", "version": "==0.5.4" }, - "future": { - "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" - ], - "version": "==0.18.2" - }, "idna": { "hashes": [ "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", @@ -125,45 +119,34 @@ }, "motor": { "hashes": [ - "sha256:462fbb824f4289481c158227a2579d6adaf1ec7c70cf7ebe60ed6ceb321e5869", - "sha256:d035c09ab422bc50bf3efb134f7405694cae76268545bd21e14fb22e2638f84e" + "sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909", + "sha256:756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4", + "sha256:97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a" ], "index": "pypi", - "version": "==2.0.0" + "version": "==2.1.0" }, "multidict": { "hashes": [ - "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", - "sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3", - "sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef", - "sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b", - "sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73", - "sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc", - "sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3", - "sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd", - "sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351", - "sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941", - "sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d", - "sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1", - "sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b", - "sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a", - "sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3", - "sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7", - "sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0", - "sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0", - "sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014", - "sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5", - "sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036", - "sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d", - "sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a", - "sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce", - "sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1", - "sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a", - "sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9", - "sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7", - "sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b" - ], - "version": "==4.5.2" + "sha256:09c19f642e055550c9319d5123221b7e07fc79bda58122aa93910e52f2ab2f29", + "sha256:0c1a5d5f7aa7189f7b83c4411c2af8f1d38d69c4360d5de3eea129c65d8d7ce2", + "sha256:12f22980e7ed0972a969520fb1e55682c9fca89a68b21b49ec43132e680be812", + "sha256:258660e9d6b52de1a75097944e12718d3aa59adc611b703361e3577d69167aaf", + "sha256:3374a23e707848f27b3438500db0c69eca82929337656fce556bd70031fbda74", + "sha256:503b7fce0054c73aa631cc910a470052df33d599f3401f3b77e54d31182525d5", + "sha256:6ce55f2c45ffc90239aab625bb1b4864eef33f73ea88487ef968291fbf09fb3f", + "sha256:725496dde5730f4ad0a627e1a58e2620c1bde0ad1c8080aae15d583eb23344ce", + "sha256:a3721078beff247d0cd4fb19d915c2c25f90907cf8d6cd49d0413a24915577c6", + "sha256:ba566518550f81daca649eded8b5c7dd09210a854637c82351410aa15c49324a", + "sha256:c42362750a51a15dc905cb891658f822ee5021bfbea898c03aa1ed833e2248a5", + "sha256:cf14aaf2ab067ca10bca0b14d5cbd751dd249e65d371734bc0e47ddd8fafc175", + "sha256:cf24e15986762f0e75a622eb19cfe39a042e952b8afba3e7408835b9af2be4fb", + "sha256:d7b6da08538302c5245cd3103f333655ba7f274915f1f5121c4f4b5fbdb3febe", + "sha256:e27e13b9ff0a914a6b8fb7e4947d4ac6be8e4f61ede17edffabd088817df9e26", + "sha256:e53b205f8afd76fc6c942ef39e8ee7c519c775d336291d32874082a87802c67c", + "sha256:ec804fc5f68695d91c24d716020278fcffd50890492690a7e1fef2e741f7172c" + ], + "version": "==4.7.1" }, "natural": { "hashes": [ @@ -174,11 +157,11 @@ }, "parsedatetime": { "hashes": [ - "sha256:3d817c58fb9570d1eec1dd46fa9448cd644eeed4fb612684b02dfda3a79cb84b", - "sha256:9ee3529454bf35c40a77115f5a596771e59e1aee8c53306f346c461b8e913094" + "sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", + "sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667" ], "index": "pypi", - "version": "==2.4" + "version": "==2.5" }, "pipenv": { "hashes": [ @@ -191,36 +174,61 @@ }, "pymongo": { "hashes": [ - "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" + "sha256:0369136c6e79c5edc16aa5de2b48a1b1c1fe5e6f7fc5915a2deaa98bd6e9dad5", + "sha256:08364e1bea1507c516b18b826ec790cb90433aec2f235033ec5eecfd1011633b", + "sha256:0af1d2bc8cc9503bf92ec3669a77ec3a6d7938193b583fb867b7e9696eed52e8", + "sha256:0cfd1aeeb8c0a634646ab3ebeb4ce6828b94b2e33553a69ff7e6c07c250bf201", + "sha256:1b4a13dff15641e58620524db15d7a323d60572b2b187261c5cb58c36d74778d", + "sha256:22fbdb908257f9aaaa372a7684f3e094a05ca52eb84f8f381c8b1827c49556fd", + "sha256:264272fd1c95fc48002ad85d5e41270831777b4180f2500943e45e12b2a3ab43", + "sha256:3372e98eebbfd05ebf020388003f8a4438bed41e0fef1ef696d2c13633c416c8", + "sha256:339d24ecdc42745d2dc09b26fda8151988e806ca81134a7bd10513c4031d91e1", + "sha256:38281855fc3961ba5510fbb503b8d16cc1fcb326e9f7ba0dd096ed4eb72a7084", + "sha256:4acdd2e16392472bfd49ca49038845c95e5254b5af862b55f7f2cc79aa258886", + "sha256:4e0c006bc6e98e861b678432e05bf64ba3eb889b6ab7e7bf1ebaecf9f1ba0e58", + "sha256:4e4284bcbe4b7be1b37f9641509085b715c478e7fbf8f820358362b5dd359379", + "sha256:4e5e94a5f9823f0bd0c56012a57650bc6772636c29d83d253260c26b908fcfd9", + "sha256:4e61f30800a40f1770b2ec56bbf5dc0f0e3f7e9250eb05fa4feb9ccb7bbe39ca", + "sha256:53577cf57ba9d93b58ab41d45250277828ff83c5286dde14f855e4b17ec19976", + "sha256:681cb31e8631882804a6cc3c8cc8f54a74ff3a82261a78e50f20c5eec05ac855", + "sha256:6dfc2710f43dd1d66991a0f160d196356732ccc8aa9dbc6875aeba78388fa142", + "sha256:72218201b13d8169be5736417987e9a0a3b10d4349e40e4db7a6a5ac670c7ef2", + "sha256:7247fbcdbf7ab574eb70743461b3cfc14d9cfae3f27a9afb6ce14d87f67dd0b5", + "sha256:72651f4b4adf50201891580506c8cca465d94d38f26ed92abfc56440662c723c", + "sha256:87b3aaf12ad6a9b5570b12d2a4b8802757cb3588a903aafd3c25f07f9caf07e3", + "sha256:87c28b7b37617c5a01eb396487f7d3b61a453e1fa0475a175ab87712d6f5d52f", + "sha256:88efe627b628f36ef53f09abb218d4630f83d8ebde7028689439559475c43dae", + "sha256:89bfbca22266f12df7fb80092b7c876734751d02b93789580b68957ad4a8bf56", + "sha256:908a3caf348a672b28b8a06fe7b4a27c2fdcf7f873df671e4027d48bcd7f971f", + "sha256:9128e7bea85f3a3041306fa14a7aa82a24b47881918500e1b8396dd1c933b5a6", + "sha256:9737d6d688a15b8d5c0bfa909638b79261e195be817b9f1be79c722bbb23cd76", + "sha256:98a8305da158f46e99e7e51db49a2f8b5fcdd7683ea7083988ccb9c4450507a6", + "sha256:99285cd44c756f0900cbdb5fe75f567c0a76a273b7e0467f23cb76f47e60aac0", + "sha256:9ed568f8026ffeb00ce31e5351e0d09d704cc19a29549ba4da0ac145d2a26fdf", + "sha256:a006162035032021dfd00a879643dc06863dac275f9210d843278566c719eebc", + "sha256:a03cb336bc8d25a11ff33b94967478a9775b0d2b23b39e952d9cc6cb93b75d69", + "sha256:a863ceb67be163060d1099b7e89b6dd83d6dd50077c7ceae31ac844c4c2baff9", + "sha256:b82628eaf0a16c1f50e1c205fd1dd406d7874037dd84643da89e91b5043b5e82", + "sha256:bc6446a41fb7eeaf2c808bab961b9bac81db0f5de69eab74eebe1b8b072399f7", + "sha256:c42d290ed54096355838421cf9d2a56e150cb533304d2439ef1adf612a986eaf", + "sha256:c43879fe427ea6aa6e84dae9fbdc5aa14428a4cfe613fe0fee2cc004bf3f307c", + "sha256:c566cbdd1863ba3ccf838656a1403c3c81fdb57cbe3fdd3515be7c9616763d33", + "sha256:c5b7a0d7e6ca986de32b269b6dbbd5162c1a776ece72936f55decb4d1b197ee9", + "sha256:ca109fe9f74da4930590bb589eb8fdf80e5d19f5cd9f337815cac9309bbd0a76", + "sha256:d0260ba68f9bafd8775b2988b5aeace6e69a37593ec256e23e150c808160c05c", + "sha256:d2ce33501149b373118fcfec88a292a87ef0b333fb30c7c6aac72fe64700bdf6", + "sha256:d582ea8496e2a0e124e927a67dca55c8833f0dbfbc2c84aaf0e5949a2dd30c51", + "sha256:d68b9ab0a900582a345fb279675b0ad4fac07d6a8c2678f12910d55083b7240d", + "sha256:dbf1fa571db6006907aeaf6473580aaa76041f4f3cd1ff8a0039fd0f40b83f6d", + "sha256:e032437a7d2b89dab880c79379d88059cee8019da0ff475d924c4ccab52db88f", + "sha256:e0f5798f3ad60695465a093e3d002f609c41fef3dcb97fcefae355d24d3274cf", + "sha256:e756355704a2cf91a7f4a649aa0bbf3bbd263018b9ed08f60198c262f4ee24b6", + "sha256:e824b4b87bd88cbeb25c8babeadbbaaaf06f02bbb95a93462b7c6193a064974e", + "sha256:ea1171470b52487152ed8bf27713cc2480dc8b0cd58e282a1bff742541efbfb8", + "sha256:fa19aef44d5ed8f798a8136ff981aedfa508edac3b1bed481eca5dde5f14fd3d", + "sha256:fceb6ae5a149a42766efb8344b0df6cfb21b55c55f360170abaddb11d43af0f1" + ], + "version": "==3.10.0" }, "python-dateutil": { "hashes": [ @@ -263,10 +271,10 @@ }, "virtualenv": { "hashes": [ - "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", - "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" + "sha256:116655188441670978117d0ebb6451eb6a7526f9ae0796cc0dee6bd7356909b0", + "sha256:b57776b44f91511866594e477dd10e76a6eb44439cdd7f06dcd30ba4c5bd854f" ], - "version": "==16.7.7" + "version": "==16.7.8" }, "virtualenv-clone": { "hashes": [ @@ -303,19 +311,25 @@ }, "yarl": { "hashes": [ - "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9", - "sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f", - "sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb", - "sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320", - "sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842", - "sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0", - "sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829", - "sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310", - "sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4", - "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8", - "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1" - ], - "version": "==1.3.0" + "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", + "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", + "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", + "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", + "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", + "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", + "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", + "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", + "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", + "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", + "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", + "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", + "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", + "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", + "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", + "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", + "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" + ], + "version": "==1.4.2" } }, "develop": { @@ -434,10 +448,10 @@ }, "pbr": { "hashes": [ - "sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", - "sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9" + "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", + "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488" ], - "version": "==5.4.3" + "version": "==5.4.4" }, "pycodestyle": { "hashes": [ @@ -463,21 +477,19 @@ }, "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" + "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", + "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", + "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", + "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", + "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", + "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", + "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", + "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", + "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", + "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", + "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" + ], + "version": "==5.2" }, "six": { "hashes": [ From ca0b61d943168a585e2972c112ea13871773e792 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Dec 2019 23:22:21 -0800 Subject: [PATCH 38/38] officially v3.4.0 --- .github/workflows/lints.yml | 12 +++------- .travis.yml | 30 ------------------------- CHANGELOG.md | 4 ++-- Pipfile | 1 - Pipfile.lock | 31 +------------------------- README.md | 2 +- bot.py | 2 +- poetry.lock | 44 +------------------------------------ pyproject.toml | 3 +-- 9 files changed, 10 insertions(+), 119 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 852b3e1e07..c84c9af773 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -13,21 +13,16 @@ jobs: # python-version: [3.6, 3.7] runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: [3.6, 3.7] - steps: - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python 3.7 uses: actions/setup-python@v1 with: - python-version: ${{ matrix.python-version }} + python-version: 3.7 - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install bandit flake8 pylint black + python -m pip install bandit pylint black continue-on-error: true - name: Bandit syntax check run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json @@ -37,4 +32,3 @@ jobs: - name: Black and flake8 run: | black . --check - flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4f7ef0508f..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -language: python - -matrix: - include: - - python: '3.7' - 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.2 - language: shell - - name: "Python 3.7.5 on Windows" - os: windows - language: shell - before_install: - - choco install python --version=3.7.5 - - 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 - - pipenv run black . --check \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6606c13c1a..54e9d761f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,8 @@ 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.4.0-dev7 +# v3.4.0 -(Development update, very likely to be unstable!) ### Added @@ -52,6 +51,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Commit to black format line width max = 99, consistent with pylint. - Alias parser is rewritten without shlex. - New checks with thread create / find. +- No more flake8 and travis. # v3.3.2 diff --git a/Pipfile b/Pipfile index 8f39ad0351..9173c3a5fe 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,6 @@ verify_ssl = true 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 7aefd57a71..06d263645b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "55c46b8ade1feae39241bbef88351ac66f48b22ef077850fe50aec9a5afa18b1" + "sha256": "c2eb0898f236534a02cb1c198d74c82fed052b4445e39f99c1af3e58d22aa435" }, "pipfile-spec": 6, "requires": { @@ -377,21 +377,6 @@ ], "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", @@ -453,20 +438,6 @@ ], "version": "==5.4.4" }, - "pycodestyle": { - "hashes": [ - "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", - "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" - ], - "version": "==2.5.0" - }, - "pyflakes": { - "hashes": [ - "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", - "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" - ], - "version": "==2.1.1" - }, "pylint": { "hashes": [ "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", diff --git a/README.md b/README.md index 039879c0cf..d162f03bc3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
diff --git a/bot.py b/bot.py index 5c25092123..b5ccdaccd5 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.4.0-dev7" +__version__ = "3.4.0" import asyncio diff --git a/poetry.lock b/poetry.lock index 568f53fa6a..1841d893a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -135,28 +135,6 @@ optional = false python-versions = "*" version = "0.5.4" -[[package]] -category = "dev" -description = "Discover and load entry points from installed packages." -name = "entrypoints" -optional = false -python-versions = ">=2.7" -version = "0.3" - -[[package]] -category = "dev" -description = "the modular source code checker: pep8, pyflakes and co" -name = "flake8" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.7.9" - -[package.dependencies] -entrypoints = ">=0.3.0,<0.4.0" -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.5.0,<2.6.0" -pyflakes = ">=2.1.0,<2.2.0" - [[package]] category = "main" description = "Backport of the concurrent.futures package from Python 3.2" @@ -277,22 +255,6 @@ optional = false python-versions = "*" version = "5.4.4" -[[package]] -category = "dev" -description = "Python style guide checker" -name = "pycodestyle" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.5.0" - -[[package]] -category = "dev" -description = "passive checker of Python programs" -name = "pyflakes" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.1.1" - [[package]] category = "dev" description = "python code static checker" @@ -424,7 +386,7 @@ idna = ">=2.0" multidict = ">=4.0" [metadata] -content-hash = "5d9df5468292faf3132d69c597109a1eac557695985d46a78a1edca3764f918e" +content-hash = "fbe9e329f33e482854cff5bf05b006de9830c2d46bf3874e2ee4f8a8da0b1797" python-versions = "^3.7" [metadata.hashes] @@ -441,8 +403,6 @@ colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "discord.py" = ["7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d"] dnspython = ["36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", "f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"] emoji = ["60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174"] -entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] -flake8 = ["45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", "49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"] futures = ["3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b", "51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"] gitdb2 = ["1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", "96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"] gitpython = ["9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", "c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245"] @@ -456,8 +416,6 @@ multidict = ["09c19f642e055550c9319d5123221b7e07fc79bda58122aa93910e52f2ab2f29", natural = ["18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2"] parsedatetime = ["3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", "d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"] pbr = ["139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", "61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"] -pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] -pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] pylint = ["3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", "886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"] pymongo = ["0369136c6e79c5edc16aa5de2b48a1b1c1fe5e6f7fc5915a2deaa98bd6e9dad5", "08364e1bea1507c516b18b826ec790cb90433aec2f235033ec5eecfd1011633b", "0af1d2bc8cc9503bf92ec3669a77ec3a6d7938193b583fb867b7e9696eed52e8", "0cfd1aeeb8c0a634646ab3ebeb4ce6828b94b2e33553a69ff7e6c07c250bf201", "15bbd2b5397f7d22498e2f2769fd698a8a247b9cc1a630ee8dabf647fb333480", "1b4a13dff15641e58620524db15d7a323d60572b2b187261c5cb58c36d74778d", "22fbdb908257f9aaaa372a7684f3e094a05ca52eb84f8f381c8b1827c49556fd", "264272fd1c95fc48002ad85d5e41270831777b4180f2500943e45e12b2a3ab43", "3372e98eebbfd05ebf020388003f8a4438bed41e0fef1ef696d2c13633c416c8", "339d24ecdc42745d2dc09b26fda8151988e806ca81134a7bd10513c4031d91e1", "38281855fc3961ba5510fbb503b8d16cc1fcb326e9f7ba0dd096ed4eb72a7084", "4acdd2e16392472bfd49ca49038845c95e5254b5af862b55f7f2cc79aa258886", "4e0c006bc6e98e861b678432e05bf64ba3eb889b6ab7e7bf1ebaecf9f1ba0e58", "4e4284bcbe4b7be1b37f9641509085b715c478e7fbf8f820358362b5dd359379", "4e5e94a5f9823f0bd0c56012a57650bc6772636c29d83d253260c26b908fcfd9", "4e61f30800a40f1770b2ec56bbf5dc0f0e3f7e9250eb05fa4feb9ccb7bbe39ca", "53577cf57ba9d93b58ab41d45250277828ff83c5286dde14f855e4b17ec19976", "681cb31e8631882804a6cc3c8cc8f54a74ff3a82261a78e50f20c5eec05ac855", "6dfc2710f43dd1d66991a0f160d196356732ccc8aa9dbc6875aeba78388fa142", "72218201b13d8169be5736417987e9a0a3b10d4349e40e4db7a6a5ac670c7ef2", "7247fbcdbf7ab574eb70743461b3cfc14d9cfae3f27a9afb6ce14d87f67dd0b5", "72651f4b4adf50201891580506c8cca465d94d38f26ed92abfc56440662c723c", "87b3aaf12ad6a9b5570b12d2a4b8802757cb3588a903aafd3c25f07f9caf07e3", "87c28b7b37617c5a01eb396487f7d3b61a453e1fa0475a175ab87712d6f5d52f", "88efe627b628f36ef53f09abb218d4630f83d8ebde7028689439559475c43dae", "89bfbca22266f12df7fb80092b7c876734751d02b93789580b68957ad4a8bf56", "908a3caf348a672b28b8a06fe7b4a27c2fdcf7f873df671e4027d48bcd7f971f", "9128e7bea85f3a3041306fa14a7aa82a24b47881918500e1b8396dd1c933b5a6", "9737d6d688a15b8d5c0bfa909638b79261e195be817b9f1be79c722bbb23cd76", "98a8305da158f46e99e7e51db49a2f8b5fcdd7683ea7083988ccb9c4450507a6", "99285cd44c756f0900cbdb5fe75f567c0a76a273b7e0467f23cb76f47e60aac0", "9ed568f8026ffeb00ce31e5351e0d09d704cc19a29549ba4da0ac145d2a26fdf", "a006162035032021dfd00a879643dc06863dac275f9210d843278566c719eebc", "a03cb336bc8d25a11ff33b94967478a9775b0d2b23b39e952d9cc6cb93b75d69", "a863ceb67be163060d1099b7e89b6dd83d6dd50077c7ceae31ac844c4c2baff9", "b82628eaf0a16c1f50e1c205fd1dd406d7874037dd84643da89e91b5043b5e82", "bc6446a41fb7eeaf2c808bab961b9bac81db0f5de69eab74eebe1b8b072399f7", "c42d290ed54096355838421cf9d2a56e150cb533304d2439ef1adf612a986eaf", "c43879fe427ea6aa6e84dae9fbdc5aa14428a4cfe613fe0fee2cc004bf3f307c", "c566cbdd1863ba3ccf838656a1403c3c81fdb57cbe3fdd3515be7c9616763d33", "c5b7a0d7e6ca986de32b269b6dbbd5162c1a776ece72936f55decb4d1b197ee9", "ca109fe9f74da4930590bb589eb8fdf80e5d19f5cd9f337815cac9309bbd0a76", "d0260ba68f9bafd8775b2988b5aeace6e69a37593ec256e23e150c808160c05c", "d12d86e771fc3072a0e6bdbf4e417c63fec85ee47cb052ba7ad239403bf5e154", "d2ce33501149b373118fcfec88a292a87ef0b333fb30c7c6aac72fe64700bdf6", "d582ea8496e2a0e124e927a67dca55c8833f0dbfbc2c84aaf0e5949a2dd30c51", "d68b9ab0a900582a345fb279675b0ad4fac07d6a8c2678f12910d55083b7240d", "dbf1fa571db6006907aeaf6473580aaa76041f4f3cd1ff8a0039fd0f40b83f6d", "e032437a7d2b89dab880c79379d88059cee8019da0ff475d924c4ccab52db88f", "e0f5798f3ad60695465a093e3d002f609c41fef3dcb97fcefae355d24d3274cf", "e756355704a2cf91a7f4a649aa0bbf3bbd263018b9ed08f60198c262f4ee24b6", "e824b4b87bd88cbeb25c8babeadbbaaaf06f02bbb95a93462b7c6193a064974e", "ea1171470b52487152ed8bf27713cc2480dc8b0cd58e282a1bff742541efbfb8", "fa19aef44d5ed8f798a8136ff981aedfa508edac3b1bed481eca5dde5f14fd3d", "faf83d20c041637cb277e5fdb59abc217c40ab3202dd87cc95d6fbd9ce5ffd9b", "fceb6ae5a149a42766efb8344b0df6cfb21b55c55f360170abaddb11d43af0f1"] python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"] diff --git a/pyproject.toml b/pyproject.toml index 9db56ce308..e9c402661d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.4.0-dev7' +version = '3.4.0' description = 'Modmail is similar to Reddits Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way.' license = 'AGPL-3.0-only' authors = [ @@ -53,5 +53,4 @@ aiohttp = "<3.6.0,>=3.3.0" black = {version = "=19.3b0", allows-prereleases = true} pylint = "^2.4" bandit = "^1.6" -flake8 = "^3.7"